Files
rippled/src/test/app/AMMExtended_test.cpp
Pratik Mankawde efa917d9f3 refactor: Retire fix1578 amendment (#5927)
Amendments activated for more than 2 years can be retired. This change retires the fix1578 amendment.
2025-10-29 16:08:17 +00:00

3864 lines
134 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/PathSet.h>
#include <test/jtx/amount.h>
#include <test/jtx/sendmax.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/app/paths/AMMContext.h>
#include <xrpld/app/paths/AMMOffer.h>
#include <xrpld/app/paths/Flow.h>
#include <xrpld/app/paths/detail/StrandFlow.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <utility>
#include <vector>
namespace ripple {
namespace test {
/**
* Tests of AMM that use offers too.
*/
struct AMMExtended_test : public jtx::AMMTest
{
private:
void
testRmFundedOffer(FeatureBitset features)
{
testcase("Incorrect Removal of Funded Offers");
// We need at least two paths. One at good quality and one at bad
// quality. The bad quality path needs two offer books in a row.
// Each offer book should have two offers at the same quality, the
// offers should be completely consumed, and the payment should
// require both offers to be satisfied. The first offer must
// be "taker gets" XRP. Ensure that the payment engine does not remove
// the first "taker gets" xrp offer, because the offer is still
// funded and not used for the payment.
using namespace jtx;
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
XRP(10'000),
{USD(200'000), BTC(2'000)});
// Must be two offers at the same quality
// "taker gets" must be XRP
// (Different amounts so I can distinguish the offers)
env(offer(carol, BTC(49), XRP(49)));
env(offer(carol, BTC(51), XRP(51)));
// Offers for the poor quality path
// Must be two offers at the same quality
env(offer(carol, XRP(50), USD(50)));
env(offer(carol, XRP(50), USD(50)));
// Good quality path
AMM ammCarol(env, carol, BTC(1'000), USD(100'100));
PathSet paths(Path(XRP, USD), Path(USD));
env(pay(alice, bob, USD(100)),
json(paths.json()),
sendmax(BTC(1'000)),
txflags(tfPartialPayment));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{BTC, UINT64_C(1'001'000000374812), -12},
USD(100'000),
ammCarol.tokens()));
}
else
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{BTC, UINT64_C(1'001'000000374815), -12},
USD(100'000),
ammCarol.tokens()));
}
env.require(balance(bob, USD(200'100)));
BEAST_EXPECT(isOffer(env, carol, BTC(49), XRP(49)));
}
void
testEnforceNoRipple(FeatureBitset features)
{
testcase("Enforce No Ripple");
using namespace jtx;
{
// No ripple with an implied account step after AMM
Env env{*this, features};
Account const dan("dan");
Account const gw1("gw1");
Account const gw2("gw2");
auto const USD1 = gw1["USD"];
auto const USD2 = gw2["USD"];
env.fund(XRP(20'000), alice, noripple(bob), carol, dan, gw1, gw2);
env.close();
env.trust(USD1(20'000), alice, carol, dan);
env(trust(bob, USD1(1'000), tfSetNoRipple));
env.trust(USD2(1'000), alice, carol, dan);
env(trust(bob, USD2(1'000), tfSetNoRipple));
env.close();
env(pay(gw1, dan, USD1(10'000)));
env(pay(gw1, bob, USD1(50)));
env(pay(gw2, bob, USD2(50)));
env.close();
AMM ammDan(env, dan, XRP(10'000), USD1(10'000));
env(pay(alice, carol, USD2(50)),
path(~USD1, bob),
sendmax(XRP(50)),
txflags(tfNoRippleDirect),
ter(tecPATH_DRY));
}
{
// Make sure payment works with default flags
Env env{*this, features};
Account const dan("dan");
Account const gw1("gw1");
Account const gw2("gw2");
auto const USD1 = gw1["USD"];
auto const USD2 = gw2["USD"];
env.fund(XRP(20'000), alice, bob, carol, gw1, gw2);
env.fund(XRP(20'000), dan);
env.close();
env.trust(USD1(20'000), alice, bob, carol, dan);
env.trust(USD2(1'000), alice, bob, carol, dan);
env.close();
env(pay(gw1, dan, USD1(10'050)));
env(pay(gw1, bob, USD1(50)));
env(pay(gw2, bob, USD2(50)));
env.close();
AMM ammDan(env, dan, XRP(10'000), USD1(10'050));
env(pay(alice, carol, USD2(50)),
path(~USD1, bob),
sendmax(XRP(50)),
txflags(tfNoRippleDirect));
BEAST_EXPECT(ammDan.expectBalances(
XRP(10'050), USD1(10'000), ammDan.tokens()));
BEAST_EXPECT(expectLedgerEntryRoot(
env, alice, XRP(20'000) - XRP(50) - txfee(env, 1)));
BEAST_EXPECT(expectHolding(env, bob, USD1(100)));
BEAST_EXPECT(expectHolding(env, bob, USD2(0)));
BEAST_EXPECT(expectHolding(env, carol, USD2(50)));
}
}
void
testFillModes(FeatureBitset features)
{
testcase("Fill Modes");
using namespace jtx;
auto const startBalance = XRP(1'000'000);
// Fill or Kill - unless we fully cross, just charge a fee and don't
// place the offer on the books. But also clean up expired offers
// that are discovered along the way.
testAMM(
[&](AMM& ammAlice, Env& env) {
// Order that can't be filled
TER const killedCode{TER{tecKILLED}};
env(offer(carol, USD(100), XRP(100)),
txflags(tfFillOrKill),
ter(killedCode));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// fee = AMM
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRP(30'000) - (txfee(env, 1))));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
// Order that can be filled
env(offer(carol, XRP(100), USD(100)),
txflags(tfFillOrKill),
ter(tesSUCCESS));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'100), ammAlice.tokens()));
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRP(30'000) + XRP(100) - txfee(env, 2)));
BEAST_EXPECT(expectHolding(env, carol, USD(29'900)));
BEAST_EXPECT(expectOffers(env, carol, 0));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
// Immediate or Cancel - cross as much as possible
// and add nothing on the books.
testAMM(
[&](AMM& ammAlice, Env& env) {
env(offer(carol, XRP(200), USD(200)),
txflags(tfImmediateOrCancel),
ter(tesSUCCESS));
// AMM generates a synthetic offer of 100USD/100XRP
// to match the CLOB offer quality.
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'100), ammAlice.tokens()));
// +AMM - offer * fee
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRP(30'000) + XRP(100) - txfee(env, 1)));
// AMM
BEAST_EXPECT(expectHolding(env, carol, USD(29'900)));
BEAST_EXPECT(expectOffers(env, carol, 0));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
// tfPassive -- place the offer without crossing it.
testAMM(
[&](AMM& ammAlice, Env& env) {
// Carol creates a passive offer that could cross AMM.
// Carol's offer should stay in the ledger.
env(offer(carol, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
BEAST_EXPECT(expectOffers(
env, carol, 1, {{{XRP(100), STAmount{USD, 100}}}}));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
// tfPassive -- cross only offers of better quality.
testAMM(
[&](AMM& ammAlice, Env& env) {
env(offer(alice, USD(110), XRP(100)));
env.close();
// Carol creates a passive offer. That offer should cross
// AMM and leave Alice's offer untouched.
env(offer(carol, XRP(100), USD(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'900),
STAmount{USD, UINT64_C(9'082'56880733945), -11},
ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, alice, 1));
},
{{XRP(11'000), USD(9'000)}},
0,
std::nullopt,
{features});
}
void
testOfferCrossWithXRP(FeatureBitset features)
{
testcase("Offer Crossing with XRP, Normal order");
using namespace jtx;
Env env{*this, features};
fund(env, gw, {bob, alice}, XRP(300'000), {USD(100)}, Fund::All);
AMM ammAlice(env, alice, XRP(150'000), USD(50));
// Existing offer pays better than this wants.
// Partially consume existing offer.
// Pay 1 USD, get 3061224490 Drops.
auto const xrpTransferred = XRPAmount{3'061'224'490};
env(offer(bob, USD(1), XRP(4'000)));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(150'000) + xrpTransferred,
USD(49),
IOUAmount{273'861'278752583, -8}));
BEAST_EXPECT(expectHolding(env, bob, STAmount{USD, 101}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(300'000) - xrpTransferred - txfee(env, 1)));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
void
testOfferCrossWithLimitOverride(FeatureBitset features)
{
testcase("Offer Crossing with Limit Override");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(200'000), gw, alice, bob);
env.close();
env(trust(alice, USD(1'000)));
env(pay(gw, alice, alice["USD"](500)));
AMM ammAlice(env, alice, XRP(150'000), USD(51));
env(offer(bob, USD(1), XRP(3'000)));
BEAST_EXPECT(
ammAlice.expectBalances(XRP(153'000), USD(50), ammAlice.tokens()));
auto jrr = ledgerEntryState(env, bob, gw, "USD");
BEAST_EXPECT(jrr[jss::node][sfBalance.fieldName][jss::value] == "-1");
jrr = ledgerEntryRoot(env, bob);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
to_string(
(XRP(200'000) - XRP(3'000) - env.current()->fees().base * 1)
.xrp()));
}
void
testCurrencyConversionEntire(FeatureBitset features)
{
testcase("Currency Conversion: Entire Offer");
using namespace jtx;
Env env{*this, features};
fund(env, gw, {alice, bob}, XRP(10'000));
env.require(owners(bob, 0));
env(trust(alice, USD(100)));
env(trust(bob, USD(1'000)));
env(pay(gw, bob, USD(1'000)));
env.require(owners(alice, 1), owners(bob, 1));
env(pay(gw, alice, alice["USD"](100)));
AMM ammBob(env, bob, USD(200), XRP(1'500));
env(pay(alice, alice, XRP(500)), sendmax(USD(100)));
BEAST_EXPECT(
ammBob.expectBalances(USD(300), XRP(1'000), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, alice, USD(0)));
auto jrr = ledgerEntryRoot(env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] ==
to_string((XRP(10'000) + XRP(500) - env.current()->fees().base * 2)
.xrp()));
}
void
testCurrencyConversionInParts(FeatureBitset features)
{
testcase("Currency Conversion: In Parts");
using namespace jtx;
testAMM(
[&](AMM& ammAlice, Env& env) {
// Alice converts USD to XRP which should fail
// due to PartialPayment.
env(pay(alice, alice, XRP(100)),
sendmax(USD(100)),
ter(tecPATH_PARTIAL));
// Alice converts USD to XRP, should succeed because
// we permit partial payment
env(pay(alice, alice, XRP(100)),
sendmax(USD(100)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{9'900'990'100}, USD(10'100), ammAlice.tokens()));
// initial 30,000 - 10,000AMM - 100pay
BEAST_EXPECT(expectHolding(env, alice, USD(19'900)));
// initial 30,000 - 10,0000AMM + 99.009900pay - fee*3
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
XRP(30'000) - XRP(10'000) + XRPAmount{99'009'900} -
ammCrtFee(env) - txfee(env, 2)));
},
{{XRP(10'000), USD(10'000)}},
0,
std::nullopt,
{features});
}
void
testCrossCurrencyStartXRP(FeatureBitset features)
{
testcase("Cross Currency Payment: Start with XRP");
using namespace jtx;
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
env.close();
env(trust(bob, USD(100)));
env.close();
env(pay(alice, bob, USD(100)), sendmax(XRP(100)));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, bob, USD(100)));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
}
void
testCrossCurrencyEndXRP(FeatureBitset features)
{
testcase("Cross Currency Payment: End with XRP");
using namespace jtx;
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
env.close();
env(trust(bob, USD(100)));
env.close();
env(pay(alice, bob, XRP(100)), sendmax(USD(100)));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'100), ammAlice.tokens()));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(1'000) + XRP(100) - txfee(env, 1)));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
}
void
testCrossCurrencyBridged(FeatureBitset features)
{
testcase("Cross Currency Payment: Bridged");
using namespace jtx;
Env env{*this, features};
auto const gw1 = Account{"gateway_1"};
auto const gw2 = Account{"gateway_2"};
auto const dan = Account{"dan"};
auto const USD1 = gw1["USD"];
auto const EUR1 = gw2["EUR"];
fund(env, gw1, {gw2, alice, bob, carol, dan}, XRP(60'000));
env(trust(alice, USD1(1'000)));
env.close();
env(trust(bob, EUR1(1'000)));
env.close();
env(trust(carol, USD1(10'000)));
env.close();
env(trust(dan, EUR1(1'000)));
env.close();
env(pay(gw1, alice, alice["USD"](500)));
env.close();
env(pay(gw1, carol, carol["USD"](6'000)));
env(pay(gw2, dan, dan["EUR"](400)));
env.close();
AMM ammCarol(env, carol, USD1(5'000), XRP(50'000));
env(offer(dan, XRP(500), EUR1(50)));
env.close();
Json::Value jtp{Json::arrayValue};
jtp[0u][0u][jss::currency] = "XRP";
env(pay(alice, bob, EUR1(30)),
json(jss::Paths, jtp),
sendmax(USD1(333)));
env.close();
BEAST_EXPECT(ammCarol.expectBalances(
XRP(49'700),
STAmount{USD1, UINT64_C(5'030'181086519115), -12},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), EUR(20)}}}));
BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR1, 30}));
}
void
testOfferFeesConsumeFunds(FeatureBitset features)
{
testcase("Offer Fees Consume Funds");
using namespace jtx;
Env env{*this, features};
auto const gw1 = Account{"gateway_1"};
auto const gw2 = Account{"gateway_2"};
auto const gw3 = Account{"gateway_3"};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const USD1 = gw1["USD"];
auto const USD2 = gw2["USD"];
auto const USD3 = gw3["USD"];
// Provide micro amounts to compensate for fees to make results round
// nice.
// reserve: Alice has 3 entries in the ledger, via trust lines
// fees:
// 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) +
// 1 for payment == 4
auto const starting_xrp = XRP(100) +
env.current()->fees().accountReserve(3) +
env.current()->fees().base * 4;
env.fund(starting_xrp, gw1, gw2, gw3, alice);
env.fund(XRP(2'000), bob);
env.close();
env(trust(alice, USD1(1'000)));
env(trust(alice, USD2(1'000)));
env(trust(alice, USD3(1'000)));
env(trust(bob, USD1(1'200)));
env(trust(bob, USD2(1'100)));
env(pay(gw1, bob, bob["USD"](1'200)));
AMM ammBob(env, bob, XRP(1'000), USD1(1'200));
// Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
env(offer(alice, USD1(200), XRP(200)));
// The pool gets only 100XRP for ~109.09USD, even though
// it can exchange more.
BEAST_EXPECT(ammBob.expectBalances(
XRP(1'100),
STAmount{USD1, UINT64_C(1'090'909090909091), -12},
ammBob.tokens()));
auto jrr = ledgerEntryState(env, alice, gw1, "USD");
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] ==
"109.090909090909");
jrr = ledgerEntryRoot(env, alice);
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName] == XRP(350).value().getText());
}
void
testOfferCreateThenCross(FeatureBitset features)
{
testcase("Offer Create, then Cross");
using namespace jtx;
Env env{*this, features};
fund(env, gw, {alice, bob}, XRP(200'000));
env(rate(gw, 1.005));
env(trust(alice, USD(1'000)));
env(trust(bob, USD(1'000)));
env(pay(gw, bob, USD(1)));
env(pay(gw, alice, USD(200)));
AMM ammAlice(env, alice, USD(150), XRP(150'100));
env(offer(bob, XRP(100), USD(0.1)));
BEAST_EXPECT(ammAlice.expectBalances(
USD(150.1), XRP(150'000), ammAlice.tokens()));
auto const jrr = ledgerEntryState(env, bob, gw, "USD");
// Bob pays 0.005 transfer fee. Note 10**-10 round-off.
BEAST_EXPECT(
jrr[jss::node][sfBalance.fieldName][jss::value] == "-0.8995000001");
}
void
testSellFlagBasic(FeatureBitset features)
{
testcase("Offer tfSell: Basic Sell");
using namespace jtx;
testAMM(
[&](AMM& ammAlice, Env& env) {
env(offer(carol, USD(100), XRP(100)), json(jss::Flags, tfSell));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(9'999), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectHolding(env, carol, USD(30'101)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRP(30'000) - XRP(100) - txfee(env, 1)));
},
{{XRP(9'900), USD(10'100)}},
0,
std::nullopt,
{features});
}
void
testSellFlagExceedLimit(FeatureBitset features)
{
testcase("Offer tfSell: 2x Sell Exceed Limit");
using namespace jtx;
Env env{*this, features};
auto const starting_xrp =
XRP(100) + reserve(env, 1) + env.current()->fees().base * 2;
env.fund(starting_xrp, gw, alice);
env.fund(XRP(2'000), bob);
env.close();
env(trust(alice, USD(150)));
env(trust(bob, USD(4'000)));
env(pay(gw, bob, bob["USD"](2'200)));
AMM ammBob(env, bob, XRP(1'000), USD(2'200));
// Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
// Taker pays 100 USD for 100 XRP.
// Selling XRP.
// Will sell all 100 XRP and get more USD than asked for.
env(offer(alice, USD(100), XRP(200)), json(jss::Flags, tfSell));
BEAST_EXPECT(
ammBob.expectBalances(XRP(1'100), USD(2'000), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, alice, USD(200)));
BEAST_EXPECT(expectLedgerEntryRoot(env, alice, XRP(250)));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
void
testGatewayCrossCurrency(FeatureBitset features)
{
testcase("Client Issue: Gateway Cross Currency");
using namespace jtx;
Env env{*this, features};
auto const XTS = gw["XTS"];
auto const XXX = gw["XXX"];
auto const starting_xrp =
XRP(100.1) + reserve(env, 1) + env.current()->fees().base * 2;
fund(
env,
gw,
{alice, bob},
starting_xrp,
{XTS(100), XXX(100)},
Fund::All);
AMM ammAlice(env, alice, XTS(100), XXX(100));
Json::Value payment;
payment[jss::secret] = toBase58(generateSeed("bob"));
payment[jss::id] = env.seq(bob);
payment[jss::build_path] = true;
payment[jss::tx_json] = pay(bob, bob, bob["XXX"](1));
payment[jss::tx_json][jss::Sequence] =
env.current()
->read(keylet::account(bob.id()))
->getFieldU32(sfSequence);
payment[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
payment[jss::tx_json][jss::SendMax] =
bob["XTS"](1.5).value().getJson(JsonOptions::none);
payment[jss::tx_json][jss::Flags] = tfPartialPayment;
auto const jrr = env.rpc("json", "submit", to_string(payment));
BEAST_EXPECT(jrr[jss::result][jss::status] == "success");
BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount(XTS, UINT64_C(101'010101010101), -12),
XXX(99),
ammAlice.tokens()));
BEAST_EXPECT(expectHolding(
env, bob, STAmount{XTS, UINT64_C(98'989898989899), -12}));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount(XTS, UINT64_C(101'0101010101011), -13),
XXX(99),
ammAlice.tokens()));
BEAST_EXPECT(expectHolding(
env, bob, STAmount{XTS, UINT64_C(98'9898989898989), -13}));
}
BEAST_EXPECT(expectHolding(env, bob, XXX(101)));
}
void
testBridgedCross(FeatureBitset features)
{
testcase("Bridged Crossing");
using namespace jtx;
{
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
{USD(15'000), EUR(15'000)},
Fund::All);
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP AMM is created.
// o carol has EUR but wants USD.
// Note that carol's offer must come last. If carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
AMM ammBob(env, bob, EUR(10'000), XRP(10'100));
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(ammBob.expectBalances(
XRP(10'000), EUR(10'100), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(15'100)));
BEAST_EXPECT(expectHolding(env, carol, EUR(14'900)));
BEAST_EXPECT(expectOffers(env, carol, 0));
}
{
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
{USD(15'000), EUR(15'000)},
Fund::All);
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP offer is created.
// o carol has EUR but wants USD.
// Note that carol's offer must come last. If carol's offer is
// placed before AMM and bob's offer are created, then autobridging
// will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
env(offer(bob, EUR(100), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(15'100)));
BEAST_EXPECT(expectHolding(env, carol, EUR(14'900)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
{
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
{USD(15'000), EUR(15'000)},
Fund::All);
// The scenario:
// o USD/XRP offer is created.
// o EUR/XRP AMM is created.
// o carol has EUR but wants USD.
// Note that carol's offer must come last. If carol's offer is
// placed before AMM and alice's offer are created, then
// autobridging will not occur.
env(offer(alice, XRP(100), USD(100)));
env.close();
AMM ammBob(env, bob, EUR(10'000), XRP(10'100));
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
BEAST_EXPECT(ammBob.expectBalances(
XRP(10'000), EUR(10'100), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(15'100)));
BEAST_EXPECT(expectHolding(env, carol, EUR(14'900)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
}
void
testSellWithFillOrKill(FeatureBitset features)
{
// Test a number of different corner cases regarding offer crossing
// when both the tfSell flag and tfFillOrKill flags are set.
testcase("Combine tfSell with tfFillOrKill");
using namespace jtx;
// Code returned if an offer is killed.
TER const killedCode{TER{tecKILLED}};
{
Env env{*this, features};
fund(env, gw, {alice, bob}, {USD(20'000)}, Fund::All);
AMM ammBob(env, bob, XRP(20'000), USD(200));
// alice submits a tfSell | tfFillOrKill offer that does not cross.
env(offer(alice, USD(2.1), XRP(210), tfSell | tfFillOrKill),
ter(killedCode));
BEAST_EXPECT(
ammBob.expectBalances(XRP(20'000), USD(200), ammBob.tokens()));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
{
Env env{*this, features};
fund(env, gw, {alice, bob}, {USD(1'000)}, Fund::All);
AMM ammBob(env, bob, XRP(20'000), USD(200));
// alice submits a tfSell | tfFillOrKill offer that crosses.
// Even though tfSell is present it doesn't matter this time.
env(offer(alice, USD(2), XRP(220), tfSell | tfFillOrKill));
env.close();
BEAST_EXPECT(ammBob.expectBalances(
XRP(20'220),
STAmount{USD, UINT64_C(197'8239366963403), -13},
ammBob.tokens()));
BEAST_EXPECT(expectHolding(
env, alice, STAmount{USD, UINT64_C(1'002'17606330366), -11}));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
{
// alice submits a tfSell | tfFillOrKill offer that crosses and
// returns more than was asked for (because of the tfSell flag).
Env env{*this, features};
fund(env, gw, {alice, bob}, {USD(1'000)}, Fund::All);
AMM ammBob(env, bob, XRP(20'000), USD(200));
env(offer(alice, USD(10), XRP(1'500), tfSell | tfFillOrKill));
env.close();
BEAST_EXPECT(ammBob.expectBalances(
XRP(21'500),
STAmount{USD, UINT64_C(186'046511627907), -12},
ammBob.tokens()));
BEAST_EXPECT(expectHolding(
env, alice, STAmount{USD, UINT64_C(1'013'953488372093), -12}));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
{
// alice submits a tfSell | tfFillOrKill offer that doesn't cross.
// This would have succeeded with a regular tfSell, but the
// fillOrKill prevents the transaction from crossing since not
// all of the offer is consumed because AMM generated offer,
// which matches alice's offer quality is ~ 10XRP/0.01996USD.
Env env{*this, features};
fund(env, gw, {alice, bob}, {USD(10'000)}, Fund::All);
AMM ammBob(env, bob, XRP(5000), USD(10));
env(offer(alice, USD(1), XRP(501), tfSell | tfFillOrKill),
ter(tecKILLED));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
}
void
testTransferRateOffer(FeatureBitset features)
{
testcase("Transfer Rate Offer");
using namespace jtx;
// AMM XRP/USD. Alice places USD/XRP offer.
testAMM(
[&](AMM& ammAlice, Env& env) {
env(rate(gw, 1.25));
env.close();
env(offer(carol, USD(100), XRP(100)));
env.close();
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
BEAST_EXPECT(expectOffers(env, carol, 0));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// Reverse the order, so the offer in the books is to sell XRP
// in return for USD.
testAMM(
[&](AMM& ammAlice, Env& env) {
env(rate(gw, 1.25));
env.close();
env(offer(carol, XRP(100), USD(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'100), ammAlice.tokens()));
// Carol pays 25% transfer fee
BEAST_EXPECT(expectHolding(env, carol, USD(29'875)));
BEAST_EXPECT(expectOffers(env, carol, 0));
},
{{XRP(10'100), USD(10'000)}},
0,
std::nullopt,
{features});
{
// Bridged crossing.
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
{USD(15'000), EUR(15'000)},
Fund::All);
env(rate(gw, 1.25));
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP Offer is created.
// o carol has EUR but wants USD.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
env(offer(bob, EUR(100), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Bob's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(15'100)));
// Carol pays 25% transfer fee.
BEAST_EXPECT(expectHolding(env, carol, EUR(14'875)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
{
// Bridged crossing. The transfer fee is paid on the step not
// involving AMM as src/dst.
Env env{*this, features};
fund(
env,
gw,
{alice, bob, carol},
{USD(15'000), EUR(15'000)},
Fund::All);
env(rate(gw, 1.25));
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP Offer is created.
// o carol has EUR but wants USD.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'050));
env(offer(bob, EUR(100), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// partially consumes Bob's offer.
env(offer(carol, USD(50), EUR(50)));
env.close();
// This test verifies that the amount removed from an offer
// accounts for the transfer fee that is removed from the
// account but not from the remaining offer.
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'050), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(15'050)));
// Carol pays 25% transfer fee.
BEAST_EXPECT(expectHolding(env, carol, EUR(14'937.5)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(
expectOffers(env, bob, 1, {{Amounts{EUR(50), XRP(50)}}}));
}
{
// A trust line's QualityIn should not affect offer crossing.
// Bridged crossing. The transfer fee is paid on the step not
// involving AMM as src/dst.
Env env{*this, features};
fund(env, gw, {alice, carol, bob}, XRP(30'000));
env(rate(gw, 1.25));
env(trust(alice, USD(15'000)));
env(trust(bob, EUR(15'000)));
env(trust(carol, EUR(15'000)), qualityInPercent(80));
env(trust(bob, USD(15'000)));
env(trust(carol, USD(15'000)));
env.close();
env(pay(gw, alice, USD(11'000)));
env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000)));
env.close();
// 1000 / 0.8
BEAST_EXPECT(expectHolding(env, carol, EUR(1'250)));
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP Offer is created.
// o carol has EUR but wants USD.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
env(offer(bob, EUR(100), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Bob's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(100)));
// Carol pays 25% transfer fee: 1250 - 100(offer) - 25(transfer fee)
BEAST_EXPECT(expectHolding(env, carol, EUR(1'125)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
{
// A trust line's QualityOut should not affect offer crossing.
// Bridged crossing. The transfer fee is paid on the step not
// involving AMM as src/dst.
Env env{*this, features};
fund(env, gw, {alice, carol, bob}, XRP(30'000));
env(rate(gw, 1.25));
env(trust(alice, USD(15'000)));
env(trust(bob, EUR(15'000)));
env(trust(carol, EUR(15'000)), qualityOutPercent(120));
env(trust(bob, USD(15'000)));
env(trust(carol, USD(15'000)));
env.close();
env(pay(gw, alice, USD(11'000)));
env(pay(gw, carol, EUR(1'000)), sendmax(EUR(10'000)));
env.close();
BEAST_EXPECT(expectHolding(env, carol, EUR(1'000)));
// The scenario:
// o USD/XRP AMM is created.
// o EUR/XRP Offer is created.
// o carol has EUR but wants USD.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
env(offer(bob, EUR(100), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Bob's offer.
env(offer(carol, USD(100), EUR(100)));
env.close();
// AMM pay doesn't transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(100)));
// Carol pays 25% transfer fee: 1000 - 100(offer) - 25(transfer fee)
BEAST_EXPECT(expectHolding(env, carol, EUR(875)));
BEAST_EXPECT(expectOffers(env, carol, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
}
void
testSelfIssueOffer(FeatureBitset features)
{
// This test is not the same as corresponding testSelfIssueOffer()
// in the Offer_test. It simply tests AMM with self issue and
// offer crossing.
using namespace jtx;
Env env{*this, features};
auto const USD_bob = bob["USD"];
auto const f = env.current()->fees().base;
env.fund(XRP(30'000) + f, alice, bob);
env.close();
AMM ammBob(env, bob, XRP(10'000), USD_bob(10'100));
env(offer(alice, USD_bob(100), XRP(100)));
env.close();
BEAST_EXPECT(ammBob.expectBalances(
XRP(10'100), USD_bob(10'000), ammBob.tokens()));
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectHolding(env, alice, USD_bob(100)));
}
void
testBadPathAssert(FeatureBitset features)
{
// At one point in the past this invalid path caused assert. It
// should not be possible for user-supplied data to cause assert.
// Make sure assert is gone.
testcase("Bad path assert");
using namespace jtx;
Env env{*this, features};
// The fee that's charged for transactions.
auto const fee = env.current()->fees().base;
{
// A trust line's QualityOut should not affect offer crossing.
auto const ann = Account("ann");
auto const A_BUX = ann["BUX"];
auto const bob = Account("bob");
auto const cam = Account("cam");
auto const dan = Account("dan");
auto const D_BUX = dan["BUX"];
// Verify trust line QualityOut affects payments.
env.fund(reserve(env, 4) + (fee * 4), ann, bob, cam, dan);
env.close();
env(trust(bob, A_BUX(400)));
env(trust(bob, D_BUX(200)), qualityOutPercent(120));
env(trust(cam, D_BUX(100)));
env.close();
env(pay(dan, bob, D_BUX(100)));
env.close();
BEAST_EXPECT(expectHolding(env, bob, D_BUX(100)));
env(pay(ann, cam, D_BUX(60)), path(bob, dan), sendmax(A_BUX(200)));
env.close();
BEAST_EXPECT(expectHolding(env, ann, A_BUX(none)));
BEAST_EXPECT(expectHolding(env, ann, D_BUX(none)));
BEAST_EXPECT(expectHolding(env, bob, A_BUX(72)));
BEAST_EXPECT(expectHolding(env, bob, D_BUX(40)));
BEAST_EXPECT(expectHolding(env, cam, A_BUX(none)));
BEAST_EXPECT(expectHolding(env, cam, D_BUX(60)));
BEAST_EXPECT(expectHolding(env, dan, A_BUX(none)));
BEAST_EXPECT(expectHolding(env, dan, D_BUX(none)));
AMM ammBob(env, bob, A_BUX(30), D_BUX(30));
env(trust(ann, D_BUX(100)));
env.close();
// This payment caused the assert.
env(pay(ann, ann, D_BUX(30)),
path(A_BUX, D_BUX),
sendmax(A_BUX(30)),
ter(temBAD_PATH));
env.close();
BEAST_EXPECT(
ammBob.expectBalances(A_BUX(30), D_BUX(30), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, ann, A_BUX(none)));
BEAST_EXPECT(expectHolding(env, ann, D_BUX(0)));
BEAST_EXPECT(expectHolding(env, cam, A_BUX(none)));
BEAST_EXPECT(expectHolding(env, cam, D_BUX(60)));
BEAST_EXPECT(expectHolding(env, dan, A_BUX(0)));
BEAST_EXPECT(expectHolding(env, dan, D_BUX(none)));
}
}
void
testDirectToDirectPath(FeatureBitset features)
{
// The offer crossing code expects that a DirectStep is always
// preceded by a BookStep. In one instance the default path
// was not matching that assumption. Here we recreate that case
// so we can prove the bug stays fixed.
testcase("Direct to Direct path");
using namespace jtx;
Env env{*this, features};
auto const ann = Account("ann");
auto const bob = Account("bob");
auto const cam = Account("cam");
auto const carol = Account("carol");
auto const A_BUX = ann["BUX"];
auto const B_BUX = bob["BUX"];
auto const fee = env.current()->fees().base;
env.fund(XRP(1'000), carol);
env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam);
env.close();
env(trust(ann, B_BUX(40)));
env(trust(cam, A_BUX(40)));
env(trust(bob, A_BUX(30)));
env(trust(cam, B_BUX(40)));
env(trust(carol, B_BUX(400)));
env(trust(carol, A_BUX(400)));
env.close();
env(pay(ann, cam, A_BUX(35)));
env(pay(bob, cam, B_BUX(35)));
env(pay(bob, carol, B_BUX(400)));
env(pay(ann, carol, A_BUX(400)));
AMM ammCarol(env, carol, A_BUX(300), B_BUX(330));
// cam puts an offer on the books that her upcoming offer could cross.
// But this offer should be deleted, not crossed, by her upcoming
// offer.
env(offer(cam, A_BUX(29), B_BUX(30), tfPassive));
env.close();
env.require(balance(cam, A_BUX(35)));
env.require(balance(cam, B_BUX(35)));
env.require(offers(cam, 1));
// This offer caused the assert.
env(offer(cam, B_BUX(30), A_BUX(30)));
// AMM is consumed up to the first cam Offer quality
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{A_BUX, UINT64_C(309'3541659651605), -13},
STAmount{B_BUX, UINT64_C(320'0215509984417), -13},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env,
cam,
1,
{{Amounts{
STAmount{B_BUX, UINT64_C(20'0215509984417), -13},
STAmount{A_BUX, UINT64_C(20'0215509984417), -13}}}}));
}
else
{
BEAST_EXPECT(ammCarol.expectBalances(
STAmount{A_BUX, UINT64_C(309'3541659651604), -13},
STAmount{B_BUX, UINT64_C(320'0215509984419), -13},
ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env,
cam,
1,
{{Amounts{
STAmount{B_BUX, UINT64_C(20'0215509984419), -13},
STAmount{A_BUX, UINT64_C(20'0215509984419), -13}}}}));
}
}
void
testRequireAuth(FeatureBitset features)
{
testcase("lsfRequireAuth");
using namespace jtx;
Env env{*this, features};
auto const aliceUSD = alice["USD"];
auto const bobUSD = bob["USD"];
env.fund(XRP(400'000), gw, alice, bob);
env.close();
// GW requires authorization for holders of its IOUs
env(fset(gw, asfRequireAuth));
env.close();
// Properly set trust and have gw authorize bob and alice
env(trust(gw, bobUSD(100)), txflags(tfSetfAuth));
env(trust(bob, USD(100)));
env(trust(gw, aliceUSD(100)), txflags(tfSetfAuth));
env(trust(alice, USD(2'000)));
env(pay(gw, alice, USD(1'000)));
env.close();
// Alice is able to create AMM since the GW has authorized her
AMM ammAlice(env, alice, USD(1'000), XRP(1'050));
// Set up authorized trust line for AMM.
env(trust(gw, STAmount{Issue{USD.currency, ammAlice.ammAccount()}, 10}),
txflags(tfSetfAuth));
env.close();
env(pay(gw, bob, USD(50)));
env.close();
BEAST_EXPECT(expectHolding(env, bob, USD(50)));
// Bob's offer should cross Alice's AMM
env(offer(bob, XRP(50), USD(50)));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, bob, 0));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
}
void
testMissingAuth(FeatureBitset features)
{
testcase("Missing Auth");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(400'000), gw, alice, bob);
env.close();
// Alice doesn't have the funds
{
AMM ammAlice(
env, alice, USD(1'000), XRP(1'000), ter(tecUNFUNDED_AMM));
}
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, bob["USD"](50)), txflags(tfSetfAuth));
env.close();
env(trust(bob, USD(50)));
env.close();
env(pay(gw, bob, USD(50)));
env.close();
BEAST_EXPECT(expectHolding(env, bob, USD(50)));
// Alice should not be able to create AMM without authorization.
{
AMM ammAlice(env, alice, USD(1'000), XRP(1'000), ter(tecNO_LINE));
}
// Set up a trust line for Alice, but don't authorize it. Alice
// should still not be able to create AMM for USD/gw.
env(trust(gw, alice["USD"](2'000)));
env.close();
{
AMM ammAlice(env, alice, USD(1'000), XRP(1'000), ter(tecNO_AUTH));
}
// Finally, set up an authorized trust line for Alice. Now Alice's
// AMM create should succeed.
env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth));
env(trust(alice, USD(2'000)));
env(pay(gw, alice, USD(1'000)));
env.close();
AMM ammAlice(env, alice, USD(1'000), XRP(1'050));
// Set up authorized trust line for AMM.
env(trust(gw, STAmount{Issue{USD.currency, ammAlice.ammAccount()}, 10}),
txflags(tfSetfAuth));
env.close();
// Now bob creates his offer again, which crosses with alice's AMM.
env(offer(bob, XRP(50), USD(50)));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, bob, 0));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
}
void
testOffers()
{
using namespace jtx;
FeatureBitset const all{testable_amendments()};
testRmFundedOffer(all);
testRmFundedOffer(all - fixAMMv1_1 - fixAMMv1_3);
testEnforceNoRipple(all);
testFillModes(all);
testOfferCrossWithXRP(all);
testOfferCrossWithLimitOverride(all);
testCurrencyConversionEntire(all);
testCurrencyConversionInParts(all);
testCrossCurrencyStartXRP(all);
testCrossCurrencyEndXRP(all);
testCrossCurrencyBridged(all);
testOfferFeesConsumeFunds(all);
testOfferCreateThenCross(all);
testSellFlagExceedLimit(all);
testGatewayCrossCurrency(all);
testGatewayCrossCurrency(all - fixAMMv1_1 - fixAMMv1_3);
testBridgedCross(all);
testSellWithFillOrKill(all);
testTransferRateOffer(all);
testSelfIssueOffer(all);
testBadPathAssert(all);
testSellFlagBasic(all);
testDirectToDirectPath(all);
testDirectToDirectPath(all - fixAMMv1_1 - fixAMMv1_3);
testRequireAuth(all);
testMissingAuth(all);
}
void
path_find_consume_all()
{
testcase("path find consume all");
using namespace jtx;
Env env = pathTestEnv();
env.fund(XRP(100'000'250), alice);
fund(env, gw, {carol, bob}, {USD(100)}, Fund::All);
fund(env, gw, {alice}, {USD(100)}, Fund::IOUOnly);
AMM ammCarol(env, carol, XRP(100), USD(100));
STPathSet st;
STAmount sa;
STAmount da;
std::tie(st, sa, da) = find_paths(
env,
alice,
bob,
bob["AUD"](-1),
std::optional<STAmount>(XRP(100'000'000)));
BEAST_EXPECT(st.empty());
std::tie(st, sa, da) = find_paths(
env,
alice,
bob,
bob["USD"](-1),
std::optional<STAmount>(XRP(100'000'000)));
// Alice sends all requested 100,000,000XRP
BEAST_EXPECT(sa == XRP(100'000'000));
// Bob gets ~99.99USD. This is the amount Bob
// can get out of AMM for 100,000,000XRP.
BEAST_EXPECT(equal(
da, STAmount{bob["USD"].issue(), UINT64_C(99'9999000001), -10}));
}
// carol holds gateway AUD, sells gateway AUD for XRP
// bob will hold gateway AUD
// alice pays bob gateway AUD using XRP
void
via_offers_via_gateway()
{
testcase("via gateway");
using namespace jtx;
Env env = pathTestEnv();
auto const AUD = gw["AUD"];
env.fund(XRP(10'000), alice, bob, carol, gw);
env.close();
env(rate(gw, 1.1));
env.trust(AUD(2'000), bob, carol);
env(pay(gw, carol, AUD(51)));
env.close();
AMM ammCarol(env, carol, XRP(40), AUD(51));
env(pay(alice, bob, AUD(10)), sendmax(XRP(100)), paths(XRP));
env.close();
// AMM offer is 51.282052XRP/11AUD, 11AUD/1.1 = 10AUD to bob
BEAST_EXPECT(
ammCarol.expectBalances(XRP(51), AUD(40), ammCarol.tokens()));
BEAST_EXPECT(expectHolding(env, bob, AUD(10)));
auto const result =
find_paths(env, alice, bob, Account(bob)["USD"](25));
BEAST_EXPECT(std::get<0>(result).empty());
}
void
receive_max()
{
testcase("Receive max");
using namespace jtx;
auto const charlie = Account("charlie");
{
// XRP -> IOU receive max
Env env = pathTestEnv();
fund(env, gw, {alice, bob, charlie}, {USD(11)}, Fund::All);
AMM ammCharlie(env, charlie, XRP(10), USD(11));
auto [st, sa, da] =
find_paths(env, alice, bob, USD(-1), XRP(1).value());
BEAST_EXPECT(sa == XRP(1));
BEAST_EXPECT(equal(da, USD(1)));
if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
{
auto const& pathElem = st[0][0];
BEAST_EXPECT(
pathElem.isOffer() && pathElem.getIssuerID() == gw.id() &&
pathElem.getCurrency() == USD.currency);
}
}
{
// IOU -> XRP receive max
Env env = pathTestEnv();
fund(env, gw, {alice, bob, charlie}, {USD(11)}, Fund::All);
AMM ammCharlie(env, charlie, XRP(11), USD(10));
env.close();
auto [st, sa, da] =
find_paths(env, alice, bob, drops(-1), USD(1).value());
BEAST_EXPECT(sa == USD(1));
BEAST_EXPECT(equal(da, XRP(1)));
if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
{
auto const& pathElem = st[0][0];
BEAST_EXPECT(
pathElem.isOffer() &&
pathElem.getIssuerID() == xrpAccount() &&
pathElem.getCurrency() == xrpCurrency());
}
}
}
void
path_find_01()
{
testcase("Path Find: XRP -> XRP and XRP -> IOU");
using namespace jtx;
Env env = pathTestEnv();
Account A1{"A1"};
Account A2{"A2"};
Account A3{"A3"};
Account G1{"G1"};
Account G2{"G2"};
Account G3{"G3"};
Account M1{"M1"};
env.fund(XRP(100'000), A1);
env.fund(XRP(10'000), A2);
env.fund(XRP(1'000), A3, G1, G2, G3);
env.fund(XRP(20'000), M1);
env.close();
env.trust(G1["XYZ"](5'000), A1);
env.trust(G3["ABC"](5'000), A1);
env.trust(G2["XYZ"](5'000), A2);
env.trust(G3["ABC"](5'000), A2);
env.trust(A2["ABC"](1'000), A3);
env.trust(G1["XYZ"](100'000), M1);
env.trust(G2["XYZ"](100'000), M1);
env.trust(G3["ABC"](100'000), M1);
env.close();
env(pay(G1, A1, G1["XYZ"](3'500)));
env(pay(G3, A1, G3["ABC"](1'200)));
env(pay(G1, M1, G1["XYZ"](25'000)));
env(pay(G2, M1, G2["XYZ"](25'000)));
env(pay(G3, M1, G3["ABC"](25'000)));
env.close();
AMM ammM1_G1_G2(env, M1, G1["XYZ"](1'000), G2["XYZ"](1'000));
AMM ammM1_XRP_G3(env, M1, XRP(10'000), G3["ABC"](1'000));
STPathSet st;
STAmount sa, da;
{
auto const& send_amt = XRP(10);
std::tie(st, sa, da) =
find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(st.empty());
}
{
// no path should exist for this since dest account
// does not exist.
auto const& send_amt = XRP(200);
std::tie(st, sa, da) = find_paths(
env, A1, Account{"A0"}, send_amt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(st.empty());
}
{
auto const& send_amt = G3["ABC"](10);
std::tie(st, sa, da) =
find_paths(env, A2, G3, send_amt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, XRPAmount{101'010'102}));
BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]))));
}
{
auto const& send_amt = A2["ABC"](1);
std::tie(st, sa, da) =
find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, XRPAmount{10'010'011}));
BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3)));
}
{
auto const& send_amt = A3["ABC"](1);
std::tie(st, sa, da) =
find_paths(env, A1, A3, send_amt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, XRPAmount{10'010'011}));
BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3, A2)));
}
}
void
path_find_02()
{
testcase("Path Find: non-XRP -> XRP");
using namespace jtx;
Env env = pathTestEnv();
Account A1{"A1"};
Account A2{"A2"};
Account G3{"G3"};
Account M1{"M1"};
env.fund(XRP(1'000), A1, A2, G3);
env.fund(XRP(11'000), M1);
env.close();
env.trust(G3["ABC"](1'000), A1, A2);
env.trust(G3["ABC"](100'000), M1);
env.close();
env(pay(G3, A1, G3["ABC"](1'000)));
env(pay(G3, A2, G3["ABC"](1'000)));
env(pay(G3, M1, G3["ABC"](1'200)));
env.close();
AMM ammM1(env, M1, G3["ABC"](1'000), XRP(10'010));
STPathSet st;
STAmount sa, da;
auto const& send_amt = XRP(10);
std::tie(st, sa, da) =
find_paths(env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["ABC"](1)));
BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue()))));
}
void
path_find_05()
{
testcase("Path Find: non-XRP -> non-XRP, same currency");
using namespace jtx;
Env env = pathTestEnv();
Account A1{"A1"};
Account A2{"A2"};
Account A3{"A3"};
Account A4{"A4"};
Account G1{"G1"};
Account G2{"G2"};
Account G3{"G3"};
Account G4{"G4"};
Account M1{"M1"};
Account M2{"M2"};
env.fund(XRP(1'000), A1, A2, A3, G1, G2, G3, G4);
env.fund(XRP(10'000), A4);
env.fund(XRP(21'000), M1, M2);
env.close();
env.trust(G1["HKD"](2'000), A1);
env.trust(G2["HKD"](2'000), A2);
env.trust(G1["HKD"](2'000), A3);
env.trust(G1["HKD"](100'000), M1);
env.trust(G2["HKD"](100'000), M1);
env.trust(G1["HKD"](100'000), M2);
env.trust(G2["HKD"](100'000), M2);
env.close();
env(pay(G1, A1, G1["HKD"](1'000)));
env(pay(G2, A2, G2["HKD"](1'000)));
env(pay(G1, A3, G1["HKD"](1'000)));
env(pay(G1, M1, G1["HKD"](1'200)));
env(pay(G2, M1, G2["HKD"](5'000)));
env(pay(G1, M2, G1["HKD"](1'200)));
env(pay(G2, M2, G2["HKD"](5'000)));
env.close();
AMM ammM1(env, M1, G1["HKD"](1'010), G2["HKD"](1'000));
AMM ammM2XRP_G2(env, M2, XRP(10'000), G2["HKD"](1'010));
AMM ammM2G1_XRP(env, M2, G1["HKD"](1'010), XRP(10'000));
STPathSet st;
STAmount sa, da;
{
// A) Borrow or repay --
// Source -> Destination (repay source issuer)
auto const& send_amt = G1["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(st.empty());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["HKD"](10)));
}
{
// A2) Borrow or repay --
// Source -> Destination (repay destination issuer)
auto const& send_amt = A1["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(st.empty());
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["HKD"](10)));
}
{
// B) Common gateway --
// Source -> AC -> Destination
auto const& send_amt = A3["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, A1, A3, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["HKD"](10)));
BEAST_EXPECT(same(st, stpath(G1)));
}
{
// C) Gateway to gateway --
// Source -> OB -> Destination
auto const& send_amt = G2["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, G1, G2, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, G1["HKD"](10)));
BEAST_EXPECT(same(
st,
stpath(IPE(G2["HKD"])),
stpath(M1),
stpath(M2),
stpath(IPE(xrpIssue()), IPE(G2["HKD"]))));
}
{
// D) User to unlinked gateway via order book --
// Source -> AC -> OB -> Destination
auto const& send_amt = G2["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, A1, G2, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["HKD"](10)));
BEAST_EXPECT(same(
st,
stpath(G1, M1),
stpath(G1, M2),
stpath(G1, IPE(G2["HKD"])),
stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]))));
}
{
// I4) XRP bridge" --
// Source -> AC -> OB to XRP -> OB from XRP -> AC ->
// Destination
auto const& send_amt = A2["HKD"](10);
std::tie(st, sa, da) = find_paths(
env, A1, A2, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, A1["HKD"](10)));
BEAST_EXPECT(same(
st,
stpath(G1, M1, G2),
stpath(G1, M2, G2),
stpath(G1, IPE(G2["HKD"]), G2),
stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]), G2)));
}
}
void
path_find_06()
{
testcase("Path Find: non-XRP -> non-XRP, same currency");
using namespace jtx;
Env env = pathTestEnv();
Account A1{"A1"};
Account A2{"A2"};
Account A3{"A3"};
Account G1{"G1"};
Account G2{"G2"};
Account M1{"M1"};
env.fund(XRP(11'000), M1);
env.fund(XRP(1'000), A1, A2, A3, G1, G2);
env.close();
env.trust(G1["HKD"](2'000), A1);
env.trust(G2["HKD"](2'000), A2);
env.trust(A2["HKD"](2'000), A3);
env.trust(G1["HKD"](100'000), M1);
env.trust(G2["HKD"](100'000), M1);
env.close();
env(pay(G1, A1, G1["HKD"](1'000)));
env(pay(G2, A2, G2["HKD"](1'000)));
env(pay(G1, M1, G1["HKD"](5'000)));
env(pay(G2, M1, G2["HKD"](5'000)));
env.close();
AMM ammM1(env, M1, G1["HKD"](1'010), G2["HKD"](1'000));
// E) Gateway to user
// Source -> OB -> AC -> Destination
auto const& send_amt = A2["HKD"](10);
STPathSet st;
STAmount sa, da;
std::tie(st, sa, da) =
find_paths(env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency);
BEAST_EXPECT(equal(da, send_amt));
BEAST_EXPECT(equal(sa, G1["HKD"](10)));
BEAST_EXPECT(same(st, stpath(M1, G2), stpath(IPE(G2["HKD"]), G2)));
}
void
testFalseDry(FeatureBitset features)
{
testcase("falseDryChanges");
using namespace jtx;
Env env(*this, features);
env.fund(XRP(10'000), alice, gw);
// This removes no ripple for carol,
// different from the original test
fund(env, gw, {carol}, XRP(10'000), {}, Fund::Acct);
auto const AMMXRPPool = env.current()->fees().increment * 2;
env.fund(reserve(env, 5) + ammCrtFee(env) + AMMXRPPool, bob);
env.close();
env.trust(USD(1'000), alice, bob, carol);
env.trust(EUR(1'000), alice, bob, carol);
env(pay(gw, alice, EUR(50)));
env(pay(gw, bob, USD(150)));
// Bob has _just_ slightly less than 50 xrp available
// If his owner count changes, he will have more liquidity.
// This is one error case to test (when Flow is used).
// Computing the incoming xrp to the XRP/USD offer will require two
// recursive calls to the EUR/XRP offer. The second call will return
// tecPATH_DRY, but the entire path should not be marked as dry.
// This is the second error case to test (when flowV1 is used).
env(offer(bob, EUR(50), XRP(50)));
AMM ammBob(env, bob, AMMXRPPool, USD(150));
env(pay(alice, carol, USD(1'000'000)),
path(~XRP, ~USD),
sendmax(EUR(500)),
txflags(tfNoRippleDirect | tfPartialPayment));
auto const carolUSD = env.balance(carol, USD).value();
BEAST_EXPECT(carolUSD > USD(0) && carolUSD < USD(50));
}
void
testBookStep(FeatureBitset features)
{
testcase("Book Step");
using namespace jtx;
{
// simple IOU/IOU offer
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(10'000),
{BTC(100), USD(150)},
Fund::All);
AMM ammBob(env, bob, BTC(100), USD(150));
env(pay(alice, carol, USD(50)), path(~USD), sendmax(BTC(50)));
BEAST_EXPECT(expectHolding(env, alice, BTC(50)));
BEAST_EXPECT(expectHolding(env, bob, BTC(0)));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
BEAST_EXPECT(expectHolding(env, carol, USD(200)));
BEAST_EXPECT(
ammBob.expectBalances(BTC(150), USD(100), ammBob.tokens()));
}
{
// simple IOU/XRP XRP/IOU offer
Env env(*this, features);
fund(
env,
gw,
{alice, carol, bob},
XRP(10'000),
{BTC(100), USD(150)},
Fund::All);
AMM ammBobBTC_XRP(env, bob, BTC(100), XRP(150));
AMM ammBobXRP_USD(env, bob, XRP(100), USD(150));
env(pay(alice, carol, USD(50)), path(~XRP, ~USD), sendmax(BTC(50)));
BEAST_EXPECT(expectHolding(env, alice, BTC(50)));
BEAST_EXPECT(expectHolding(env, bob, BTC(0)));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
BEAST_EXPECT(expectHolding(env, carol, USD(200)));
BEAST_EXPECT(ammBobBTC_XRP.expectBalances(
BTC(150), XRP(100), ammBobBTC_XRP.tokens()));
BEAST_EXPECT(ammBobXRP_USD.expectBalances(
XRP(150), USD(100), ammBobXRP_USD.tokens()));
}
{
// simple XRP -> USD through offer and sendmax
Env env(*this, features);
fund(
env,
gw,
{alice, carol, bob},
XRP(10'000),
{USD(150)},
Fund::All);
AMM ammBob(env, bob, XRP(100), USD(150));
env(pay(alice, carol, USD(50)), path(~USD), sendmax(XRP(50)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, alice, xrpMinusFee(env, 10'000 - 50)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(10'000) - XRP(100) - ammCrtFee(env)));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
BEAST_EXPECT(expectHolding(env, carol, USD(200)));
BEAST_EXPECT(
ammBob.expectBalances(XRP(150), USD(100), ammBob.tokens()));
}
{
// simple USD -> XRP through offer and sendmax
Env env(*this, features);
fund(
env,
gw,
{alice, carol, bob},
XRP(10'000),
{USD(100)},
Fund::All);
AMM ammBob(env, bob, USD(100), XRP(150));
env(pay(alice, carol, XRP(50)), path(~XRP), sendmax(USD(50)));
BEAST_EXPECT(expectHolding(env, alice, USD(50)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(10'000) - XRP(150) - ammCrtFee(env)));
BEAST_EXPECT(expectHolding(env, bob, USD(0)));
BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRP(10'000 + 50)));
BEAST_EXPECT(
ammBob.expectBalances(USD(150), XRP(100), ammBob.tokens()));
}
{
// test unfunded offers are removed when payment succeeds
Env env(*this, features);
env.fund(XRP(10'000), alice, carol, gw);
env.fund(XRP(10'000), bob);
env.close();
env.trust(USD(1'000), alice, bob, carol);
env.trust(BTC(1'000), alice, bob, carol);
env.trust(EUR(1'000), alice, bob, carol);
env.close();
env(pay(gw, alice, BTC(60)));
env(pay(gw, bob, USD(200)));
env(pay(gw, bob, EUR(150)));
env.close();
env(offer(bob, BTC(50), USD(50)));
env(offer(bob, BTC(40), EUR(50)));
env.close();
AMM ammBob(env, bob, EUR(100), USD(150));
// unfund offer
env(pay(bob, gw, EUR(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(50), USD(50)));
BEAST_EXPECT(isOffer(env, bob, BTC(40), EUR(50)));
env(pay(alice, carol, USD(50)),
path(~USD),
path(~EUR, ~USD),
sendmax(BTC(60)));
env.require(balance(alice, BTC(10)));
env.require(balance(bob, BTC(50)));
env.require(balance(bob, USD(0)));
env.require(balance(bob, EUR(0)));
env.require(balance(carol, USD(50)));
// used in the payment
BEAST_EXPECT(!isOffer(env, bob, BTC(50), USD(50)));
// found unfunded
BEAST_EXPECT(!isOffer(env, bob, BTC(40), EUR(50)));
// unchanged
BEAST_EXPECT(
ammBob.expectBalances(EUR(100), USD(150), ammBob.tokens()));
}
{
// test unfunded offers are removed when the payment fails.
// bob makes two offers: a funded 50 USD for 50 BTC and an
// unfunded 50 EUR for 60 BTC. alice pays carol 61 USD with 61
// BTC. alice only has 60 BTC, so the payment will fail. The
// payment uses two paths: one through bob's funded offer and
// one through his unfunded offer. When the payment fails `flow`
// should return the unfunded offer. This test is intentionally
// similar to the one that removes unfunded offers when the
// payment succeeds.
Env env(*this, features);
env.fund(XRP(10'000), bob, carol, gw);
env.close();
// Sets rippling on, this is different from
// the original test
fund(env, gw, {alice}, XRP(10'000), {}, Fund::Acct);
env.trust(USD(1'000), alice, bob, carol);
env.trust(BTC(1'000), alice, bob, carol);
env.trust(EUR(1'000), alice, bob, carol);
env.close();
env(pay(gw, alice, BTC(60)));
env(pay(gw, bob, BTC(100)));
env(pay(gw, bob, USD(100)));
env(pay(gw, bob, EUR(50)));
env(pay(gw, carol, EUR(1)));
env.close();
// This is multiplath, which generates limited # of offers
AMM ammBobBTC_USD(env, bob, BTC(50), USD(50));
env(offer(bob, BTC(60), EUR(50)));
env(offer(carol, BTC(1'000), EUR(1)));
env(offer(bob, EUR(50), USD(50)));
// unfund offer
env(pay(bob, gw, EUR(50)));
BEAST_EXPECT(ammBobBTC_USD.expectBalances(
BTC(50), USD(50), ammBobBTC_USD.tokens()));
BEAST_EXPECT(isOffer(env, bob, BTC(60), EUR(50)));
BEAST_EXPECT(isOffer(env, carol, BTC(1'000), EUR(1)));
BEAST_EXPECT(isOffer(env, bob, EUR(50), USD(50)));
auto flowJournal = env.app().logs().journal("Flow");
auto const flowResult = [&] {
STAmount deliver(USD(51));
STAmount smax(BTC(61));
PaymentSandbox sb(env.current().get(), tapNONE);
STPathSet paths;
auto IPE = [](Issue const& iss) {
return STPathElement(
STPathElement::typeCurrency | STPathElement::typeIssuer,
xrpAccount(),
iss.currency,
iss.account);
};
{
// BTC -> USD
STPath p1({IPE(USD.issue())});
paths.push_back(p1);
// BTC -> EUR -> USD
STPath p2({IPE(EUR.issue()), IPE(USD.issue())});
paths.push_back(p2);
}
return flow(
sb,
deliver,
alice,
carol,
paths,
false,
false,
true,
OfferCrossing::no,
std::nullopt,
smax,
std::nullopt,
flowJournal);
}();
BEAST_EXPECT(flowResult.removableOffers.size() == 1);
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal j) {
if (flowResult.removableOffers.empty())
return false;
Sandbox sb(&view, tapNONE);
for (auto const& o : flowResult.removableOffers)
if (auto ok = sb.peek(keylet::offer(o)))
offerDelete(sb, ok, flowJournal);
sb.apply(view);
return true;
});
// used in payment, but since payment failed should be untouched
BEAST_EXPECT(ammBobBTC_USD.expectBalances(
BTC(50), USD(50), ammBobBTC_USD.tokens()));
BEAST_EXPECT(isOffer(env, carol, BTC(1'000), EUR(1)));
// found unfunded
BEAST_EXPECT(!isOffer(env, bob, BTC(60), EUR(50)));
}
{
// Do not produce more in the forward pass than the reverse pass
// This test uses a path that whose reverse pass will compute a
// 0.5 USD input required for a 1 EUR output. It sets a sendmax
// of 0.4 USD, so the payment engine will need to do a forward
// pass. Without limits, the 0.4 USD would produce 1000 EUR in
// the forward pass. This test checks that the payment produces
// 1 EUR, as expected.
Env env(*this, features);
env.fund(XRP(10'000), bob, carol, gw);
env.close();
fund(env, gw, {alice}, XRP(10'000), {}, Fund::Acct);
env.trust(USD(1'000), alice, bob, carol);
env.trust(EUR(1'000), alice, bob, carol);
env.close();
env(pay(gw, alice, USD(1'000)));
env(pay(gw, bob, EUR(1'000)));
env(pay(gw, bob, USD(1'000)));
env.close();
// env(offer(bob, USD(1), drops(2)), txflags(tfPassive));
AMM ammBob(env, bob, USD(8), XRPAmount{21});
env(offer(bob, drops(1), EUR(1'000)), txflags(tfPassive));
env(pay(alice, carol, EUR(1)),
path(~XRP, ~EUR),
sendmax(USD(0.4)),
txflags(tfNoRippleDirect | tfPartialPayment));
BEAST_EXPECT(expectHolding(env, carol, EUR(1)));
BEAST_EXPECT(ammBob.expectBalances(
USD(8.4), XRPAmount{20}, ammBob.tokens()));
}
}
void
testTransferRateNoOwnerFee(FeatureBitset features)
{
testcase("No Owner Fee");
using namespace jtx;
{
// payment via AMM
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(1'000),
{USD(1'000), GBP(1'000)});
env(rate(gw, 1.25));
env.close();
AMM amm(env, bob, GBP(1'000), USD(1'000));
env(pay(alice, carol, USD(100)),
path(~USD),
sendmax(GBP(150)),
txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
// alice buys 107.1428USD with 120GBP and pays 25% tr fee on 120GBP
// 1,000 - 120*1.25 = 850GBP
BEAST_EXPECT(expectHolding(env, alice, GBP(850)));
if (!features[fixAMMv1_1])
{
// 120GBP is swapped in for 107.1428USD
BEAST_EXPECT(amm.expectBalances(
GBP(1'120),
STAmount{USD, UINT64_C(892'8571428571428), -13},
amm.tokens()));
}
else
{
BEAST_EXPECT(amm.expectBalances(
GBP(1'120),
STAmount{USD, UINT64_C(892'8571428571429), -13},
amm.tokens()));
}
// 25% of 85.7142USD is paid in tr fee
// 85.7142*1.25 = 107.1428USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'085'714285714286), -12)));
}
{
// Payment via offer and AMM
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(1'000), EUR(1'000), GBP(1'000)});
env(rate(gw, 1.25));
env.close();
env(offer(ed, GBP(1'000), EUR(1'000)), txflags(tfPassive));
env.close();
AMM amm(env, bob, EUR(1'000), USD(1'000));
env(pay(alice, carol, USD(100)),
path(~EUR, ~USD),
sendmax(GBP(150)),
txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
// alice buys 120EUR with 120GBP via the offer
// and pays 25% tr fee on 120GBP
// 1,000 - 120*1.25 = 850GBP
BEAST_EXPECT(expectHolding(env, alice, GBP(850)));
// consumed offer is 120GBP/120EUR
// ed doesn't pay tr fee
BEAST_EXPECT(expectHolding(env, ed, EUR(880), GBP(1'120)));
BEAST_EXPECT(
expectOffers(env, ed, 1, {Amounts{GBP(880), EUR(880)}}));
// 25% on 96EUR is paid in tr fee 96*1.25 = 120EUR
// 96EUR is swapped in for 87.5912USD
BEAST_EXPECT(amm.expectBalances(
EUR(1'096),
STAmount{USD, UINT64_C(912'4087591240876), -13},
amm.tokens()));
// 25% on 70.0729USD is paid in tr fee 70.0729*1.25 = 87.5912USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'070'07299270073), -11)));
}
{
// Payment via AMM, AMM
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(1'000), EUR(1'000), GBP(1'000)});
env(rate(gw, 1.25));
env.close();
AMM amm1(env, bob, GBP(1'000), EUR(1'000));
AMM amm2(env, ed, EUR(1'000), USD(1'000));
env(pay(alice, carol, USD(100)),
path(~EUR, ~USD),
sendmax(GBP(150)),
txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
BEAST_EXPECT(expectHolding(env, alice, GBP(850)));
if (!features[fixAMMv1_1])
{
// alice buys 107.1428EUR with 120GBP and pays 25% tr fee on
// 120GBP 1,000 - 120*1.25 = 850GBP 120GBP is swapped in for
// 107.1428EUR
BEAST_EXPECT(amm1.expectBalances(
GBP(1'120),
STAmount{EUR, UINT64_C(892'8571428571428), -13},
amm1.tokens()));
// 25% on 85.7142EUR is paid in tr fee 85.7142*1.25 =
// 107.1428EUR 85.7142EUR is swapped in for 78.9473USD
BEAST_EXPECT(amm2.expectBalances(
STAmount(EUR, UINT64_C(1'085'714285714286), -12),
STAmount{USD, UINT64_C(921'0526315789471), -13},
amm2.tokens()));
}
else
{
// alice buys 107.1428EUR with 120GBP and pays 25% tr fee on
// 120GBP 1,000 - 120*1.25 = 850GBP 120GBP is swapped in for
// 107.1428EUR
BEAST_EXPECT(amm1.expectBalances(
GBP(1'120),
STAmount{EUR, UINT64_C(892'8571428571429), -13},
amm1.tokens()));
// 25% on 85.7142EUR is paid in tr fee 85.7142*1.25 =
// 107.1428EUR 85.7142EUR is swapped in for 78.9473USD
BEAST_EXPECT(amm2.expectBalances(
STAmount(EUR, UINT64_C(1'085'714285714286), -12),
STAmount{USD, UINT64_C(921'052631578948), -12},
amm2.tokens()));
}
// 25% on 63.1578USD is paid in tr fee 63.1578*1.25 = 78.9473USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'063'157894736842), -12)));
}
{
// AMM offer crossing
Env env(*this, features);
fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'100), EUR(1'100)});
env(rate(gw, 1.25));
env.close();
AMM amm(env, bob, USD(1'000), EUR(1'100));
env(offer(alice, EUR(100), USD(100)));
env.close();
// 100USD is swapped in for 100EUR
BEAST_EXPECT(
amm.expectBalances(USD(1'100), EUR(1'000), amm.tokens()));
// alice pays 25% tr fee on 100USD 1100-100*1.25 = 975USD
BEAST_EXPECT(expectHolding(env, alice, USD(975), EUR(1'200)));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
{
// Payment via AMM with limit quality
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(1'000),
{USD(1'000), GBP(1'000)});
env(rate(gw, 1.25));
env.close();
AMM amm(env, bob, GBP(1'000), USD(1'000));
// requested quality limit is 100USD/178.58GBP = 0.55997
// trade quality is 100USD/178.5714 = 0.55999
env(pay(alice, carol, USD(100)),
path(~USD),
sendmax(GBP(178.58)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 125USD with 142.8571GBP and pays 25% tr fee
// on 142.8571GBP
// 1,000 - 142.8571*1.25 = 821.4285GBP
BEAST_EXPECT(expectHolding(
env, alice, STAmount(GBP, UINT64_C(821'4285714285712), -13)));
// 142.8571GBP is swapped in for 125USD
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'142'857142857143), -12},
USD(875),
amm.tokens()));
// 25% on 100USD is paid in tr fee
// 100*1.25 = 125USD
BEAST_EXPECT(expectHolding(env, carol, USD(1'100)));
}
{
// Payment via AMM with limit quality, deliver less
// than requested
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(1'000),
{USD(1'200), GBP(1'200)});
env(rate(gw, 1.25));
env.close();
AMM amm(env, bob, GBP(1'000), USD(1'200));
// requested quality limit is 90USD/120GBP = 0.75
// trade quality is 22.5USD/30GBP = 0.75
env(pay(alice, carol, USD(90)),
path(~USD),
sendmax(GBP(120)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice buys 28.125USD with 24GBP and pays 25% tr fee
// on 24GBP
// 1,200 - 24*1.25 = 1,170GBP
BEAST_EXPECT(expectHolding(env, alice, GBP(1'170)));
// 24GBP is swapped in for 28.125USD
BEAST_EXPECT(amm.expectBalances(
GBP(1'024), USD(1'171.875), amm.tokens()));
}
else
{
// alice buys 28.125USD with 24GBP and pays 25% tr fee
// on 24GBP
// 1,200 - 24*1.25 =~ 1,170GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'169'999999999999), -12}));
// 24GBP is swapped in for 28.125USD
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'024'000000000001), -12},
USD(1'171.875),
amm.tokens()));
}
// 25% on 22.5USD is paid in tr fee
// 22.5*1.25 = 28.125USD
BEAST_EXPECT(expectHolding(env, carol, USD(1'222.5)));
}
{
// Payment via offer and AMM with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(1'400), EUR(1'400), GBP(1'400)});
env(rate(gw, 1.25));
env.close();
env(offer(ed, GBP(1'000), EUR(1'000)), txflags(tfPassive));
env.close();
AMM amm(env, bob, EUR(1'000), USD(1'400));
// requested quality limit is 95USD/140GBP = 0.6785
// trade quality is 59.7321USD/88.0262GBP = 0.6785
env(pay(alice, carol, USD(95)),
path(~EUR, ~USD),
sendmax(GBP(140)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice buys 70.4210EUR with 70.4210GBP via the offer
// and pays 25% tr fee on 70.4210GBP
// 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'311'973684210527), -12}));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210GBP/70.4210EUR
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{EUR, UINT64_C(1'329'578947368421), -12},
STAmount{GBP, UINT64_C(1'470'421052631579), -12}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{GBP, UINT64_C(929'5789473684212), -13},
STAmount{EUR, UINT64_C(929'5789473684212), -13}}}));
// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 = 70.4210EUR
// 56.3368EUR is swapped in for 74.6651USD
BEAST_EXPECT(amm.expectBalances(
STAmount{EUR, UINT64_C(1'056'336842105263), -12},
STAmount{USD, UINT64_C(1'325'334821428571), -12},
amm.tokens()));
}
else
{
// alice buys 70.4210EUR with 70.4210GBP via the offer
// and pays 25% tr fee on 70.4210GBP
// 1,400 - 70.4210*1.25 = 1400 - 88.0262 = 1311.9736GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'311'973684210525), -12}));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210GBP/70.4210EUR
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{EUR, UINT64_C(1'329'57894736842), -11},
STAmount{GBP, UINT64_C(1'470'42105263158), -11}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{GBP, UINT64_C(929'57894736842), -11},
STAmount{EUR, UINT64_C(929'57894736842), -11}}}));
// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25 = 70.4210EUR
// 56.3368EUR is swapped in for 74.6651USD
BEAST_EXPECT(amm.expectBalances(
STAmount{EUR, UINT64_C(1'056'336842105264), -12},
STAmount{USD, UINT64_C(1'325'334821428571), -12},
amm.tokens()));
}
// 25% on 59.7321USD is paid in tr fee 59.7321*1.25 = 74.6651USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'459'732142857143), -12)));
}
{
// Payment via AMM and offer with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(1'400), EUR(1'400), GBP(1'400)});
env(rate(gw, 1.25));
env.close();
AMM amm(env, bob, GBP(1'000), EUR(1'000));
env(offer(ed, EUR(1'000), USD(1'400)), txflags(tfPassive));
env.close();
// requested quality limit is 95USD/140GBP = 0.6785
// trade quality is 47.7857USD/70.4210GBP = 0.6785
env(pay(alice, carol, USD(95)),
path(~EUR, ~USD),
sendmax(GBP(140)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice buys 53.3322EUR with 56.3368GBP via the amm
// and pays 25% tr fee on 56.3368GBP
// 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'329'578947368421), -12}));
//// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25
///= 70.4210EUR
// 56.3368GBP is swapped in for 53.3322EUR
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'056'336842105263), -12},
STAmount{EUR, UINT64_C(946'6677295918366), -13},
amm.tokens()));
}
else
{
// alice buys 53.3322EUR with 56.3368GBP via the amm
// and pays 25% tr fee on 56.3368GBP
// 1,400 - 56.3368*1.25 = 1400 - 70.4210 = 1329.5789GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'329'57894736842), -11}));
//// 25% on 56.3368EUR is paid in tr fee 56.3368*1.25
///= 70.4210EUR
// 56.3368GBP is swapped in for 53.3322EUR
BEAST_EXPECT(amm.expectBalances(
STAmount{GBP, UINT64_C(1'056'336842105264), -12},
STAmount{EUR, UINT64_C(946'6677295918366), -13},
amm.tokens()));
}
// 25% on 42.6658EUR is paid in tr fee 42.6658*1.25 = 53.3322EUR
// 42.6658EUR/59.7321USD
BEAST_EXPECT(expectHolding(
env,
ed,
STAmount{USD, UINT64_C(1'340'267857142857), -12},
STAmount{EUR, UINT64_C(1'442'665816326531), -12}));
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{Amounts{
STAmount{EUR, UINT64_C(957'3341836734693), -13},
STAmount{USD, UINT64_C(1'340'267857142857), -12}}}));
// 25% on 47.7857USD is paid in tr fee 47.7857*1.25 = 59.7321USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'447'785714285714), -12)));
}
{
// Payment via AMM, AMM with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(1'400), EUR(1'400), GBP(1'400)});
env(rate(gw, 1.25));
env.close();
AMM amm1(env, bob, GBP(1'000), EUR(1'000));
AMM amm2(env, ed, EUR(1'000), USD(1'400));
// requested quality limit is 90USD/145GBP = 0.6206
// trade quality is 66.7432USD/107.5308GBP = 0.6206
env(pay(alice, carol, USD(90)),
path(~EUR, ~USD),
sendmax(GBP(145)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// alice buys 53.3322EUR with 107.5308GBP
// 25% on 86.0246GBP is paid in tr fee
// 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'292'469135802469), -12}));
// 86.0246GBP is swapped in for 79.2106EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'086'024691358025), -12},
STAmount{EUR, UINT64_C(920'78937795562), -11},
amm1.tokens()));
// 25% on 63.3684EUR is paid in tr fee 63.3684*1.25 = 79.2106EUR
// 63.3684EUR is swapped in for 83.4291USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'063'368497635504), -12},
STAmount{USD, UINT64_C(1'316'570881226053), -12},
amm2.tokens()));
}
else
{
// alice buys 53.3322EUR with 107.5308GBP
// 25% on 86.0246GBP is paid in tr fee
// 1,400 - 86.0246*1.25 = 1400 - 107.5308 = 1229.4691GBP
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{GBP, UINT64_C(1'292'469135802466), -12}));
// 86.0246GBP is swapped in for 79.2106EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'086'024691358027), -12},
STAmount{EUR, UINT64_C(920'7893779556188), -13},
amm1.tokens()));
// 25% on 63.3684EUR is paid in tr fee 63.3684*1.25 = 79.2106EUR
// 63.3684EUR is swapped in for 83.4291USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'063'368497635505), -12},
STAmount{USD, UINT64_C(1'316'570881226053), -12},
amm2.tokens()));
}
// 25% on 66.7432USD is paid in tr fee 66.7432*1.25 = 83.4291USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount(USD, UINT64_C(1'466'743295019157), -12)));
}
{
// Payment by the issuer via AMM, AMM with limit quality,
// deliver less than requested
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(1'000),
{USD(1'400), EUR(1'400), GBP(1'400)});
env(rate(gw, 1.25));
env.close();
AMM amm1(env, alice, GBP(1'000), EUR(1'000));
AMM amm2(env, bob, EUR(1'000), USD(1'400));
// requested quality limit is 90USD/120GBP = 0.75
// trade quality is 81.1111USD/108.1481GBP = 0.75
env(pay(gw, carol, USD(90)),
path(~EUR, ~USD),
sendmax(GBP(120)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
if (!features[fixAMMv1_1])
{
// 108.1481GBP is swapped in for 97.5935EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'108'148148148149), -12},
STAmount{EUR, UINT64_C(902'4064171122988), -13},
amm1.tokens()));
// 25% on 78.0748EUR is paid in tr fee 78.0748*1.25 = 97.5935EUR
// 78.0748EUR is swapped in for 101.3888USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'078'074866310161), -12},
STAmount{USD, UINT64_C(1'298'611111111111), -12},
amm2.tokens()));
}
else
{
// 108.1481GBP is swapped in for 97.5935EUR
BEAST_EXPECT(amm1.expectBalances(
STAmount{GBP, UINT64_C(1'108'148148148151), -12},
STAmount{EUR, UINT64_C(902'4064171122975), -13},
amm1.tokens()));
// 25% on 78.0748EUR is paid in tr fee 78.0748*1.25 = 97.5935EUR
// 78.0748EUR is swapped in for 101.3888USD
BEAST_EXPECT(amm2.expectBalances(
STAmount{EUR, UINT64_C(1'078'074866310162), -12},
STAmount{USD, UINT64_C(1'298'611111111111), -12},
amm2.tokens()));
}
// 25% on 81.1111USD is paid in tr fee 81.1111*1.25 = 101.3888USD
BEAST_EXPECT(expectHolding(
env, carol, STAmount{USD, UINT64_C(1'481'111111111111), -12}));
}
}
void
testLimitQuality()
{
// Single path with amm, offer, and limit quality. The quality limit
// is such that the first offer should be taken but the second
// should not. The total amount delivered should be the sum of the
// two offers and sendMax should be more than the first offer.
testcase("limitQuality");
using namespace jtx;
{
Env env(*this);
fund(env, gw, {alice, bob, carol}, XRP(10'000), {USD(2'000)});
AMM ammBob(env, bob, XRP(1'000), USD(1'050));
env(offer(bob, XRP(100), USD(50)));
env(pay(alice, carol, USD(100)),
path(~USD),
sendmax(XRP(100)),
txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
BEAST_EXPECT(
ammBob.expectBalances(XRP(1'050), USD(1'000), ammBob.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(2'050)));
BEAST_EXPECT(expectOffers(env, bob, 1, {{{XRP(100), USD(50)}}}));
}
}
void
testXRPPathLoop()
{
testcase("Circular XRP");
using namespace jtx;
{
// Payment path starting with XRP
Env env(*this, testable_amendments());
// Note, if alice doesn't have default ripple, then pay
// fails with tecPATH_DRY.
fund(
env,
gw,
{alice, bob},
XRP(10'000),
{USD(200), EUR(200)},
Fund::All);
AMM ammAliceXRP_USD(env, alice, XRP(100), USD(101));
AMM ammAliceXRP_EUR(env, alice, XRP(100), EUR(101));
env.close();
TER const expectedTer = TER{temBAD_PATH_LOOP};
env(pay(alice, bob, EUR(1)),
path(~USD, ~XRP, ~EUR),
sendmax(XRP(1)),
txflags(tfNoRippleDirect),
ter(expectedTer));
}
{
// Payment path ending with XRP
Env env(*this);
// Note, if alice doesn't have default ripple, then pay fails
// with tecPATH_DRY.
fund(
env,
gw,
{alice, bob},
XRP(10'000),
{USD(200), EUR(200)},
Fund::All);
AMM ammAliceXRP_USD(env, alice, XRP(100), USD(100));
AMM ammAliceXRP_EUR(env, alice, XRP(100), EUR(100));
// EUR -> //XRP -> //USD ->XRP
env(pay(alice, bob, XRP(1)),
path(~XRP, ~USD, ~XRP),
sendmax(EUR(1)),
txflags(tfNoRippleDirect),
ter(temBAD_PATH_LOOP));
}
{
// Payment where loop is formed in the middle of the path, not
// on an endpoint
auto const JPY = gw["JPY"];
Env env(*this);
// Note, if alice doesn't have default ripple, then pay fails
// with tecPATH_DRY.
fund(
env,
gw,
{alice, bob},
XRP(10'000),
{USD(200), EUR(200), JPY(200)},
Fund::All);
AMM ammAliceXRP_USD(env, alice, XRP(100), USD(100));
AMM ammAliceXRP_EUR(env, alice, XRP(100), EUR(100));
AMM ammAliceXRP_JPY(env, alice, XRP(100), JPY(100));
env(pay(alice, bob, JPY(1)),
path(~XRP, ~EUR, ~XRP, ~JPY),
sendmax(USD(1)),
txflags(tfNoRippleDirect),
ter(temBAD_PATH_LOOP));
}
}
void
testStepLimit(FeatureBitset features)
{
testcase("Step Limit");
using namespace jtx;
Env env(*this, features);
auto const dan = Account("dan");
auto const ed = Account("ed");
fund(env, gw, {ed}, XRP(100'000'000), {USD(11)});
env.fund(XRP(100'000'000), alice, bob, carol, dan);
env.close();
env.trust(USD(1), bob);
env(pay(gw, bob, USD(1)));
env.trust(USD(1), dan);
env(pay(gw, dan, USD(1)));
n_offers(env, 2'000, bob, XRP(1), USD(1));
n_offers(env, 1, dan, XRP(1), USD(1));
AMM ammEd(env, ed, XRP(9), USD(11));
// Alice offers to buy 1000 XRP for 1000 USD. She takes Bob's first
// offer, removes 999 more as unfunded, then hits the step limit.
env(offer(alice, USD(1'000), XRP(1'000)));
if (!features[fixAMMv1_1])
env.require(balance(
alice, STAmount{USD, UINT64_C(2'050126257867561), -15}));
else
env.require(balance(
alice, STAmount{USD, UINT64_C(2'050125257867587), -15}));
env.require(owners(alice, 2));
env.require(balance(bob, USD(0)));
env.require(owners(bob, 1'001));
env.require(balance(dan, USD(1)));
env.require(owners(dan, 2));
// Carol offers to buy 1000 XRP for 1000 USD. She removes Bob's next
// 1000 offers as unfunded and hits the step limit.
env(offer(carol, USD(1'000), XRP(1'000)));
env.require(balance(carol, USD(none)));
env.require(owners(carol, 1));
env.require(balance(bob, USD(0)));
env.require(owners(bob, 1));
env.require(balance(dan, USD(1)));
env.require(owners(dan, 2));
}
void
test_convert_all_of_an_asset(FeatureBitset features)
{
testcase("Convert all of an asset using DeliverMin");
using namespace jtx;
{
Env env(*this, features);
fund(env, gw, {alice, bob, carol}, XRP(10'000));
env.trust(USD(100), alice, bob, carol);
env(pay(alice, bob, USD(10)),
delivermin(USD(10)),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
delivermin(USD(-5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
delivermin(XRP(5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
delivermin(Account(carol)["USD"](5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
delivermin(USD(15)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(gw, carol, USD(50)));
AMM ammCarol(env, carol, XRP(10), USD(15));
env(pay(alice, bob, USD(10)),
paths(XRP),
delivermin(USD(7)),
txflags(tfPartialPayment),
sendmax(XRP(5)),
ter(tecPATH_PARTIAL));
env.require(balance(
alice,
drops(10'000'000'000 - env.current()->fees().base.drops())));
env.require(balance(bob, XRP(10'000)));
}
{
Env env(*this, features);
fund(env, gw, {alice, bob}, XRP(10'000));
env.trust(USD(1'100), alice, bob);
env(pay(gw, bob, USD(1'100)));
AMM ammBob(env, bob, XRP(1'000), USD(1'100));
env(pay(alice, alice, USD(10'000)),
paths(XRP),
delivermin(USD(100)),
txflags(tfPartialPayment),
sendmax(XRP(100)));
env.require(balance(alice, USD(100)));
}
{
Env env(*this, features);
fund(env, gw, {alice, bob, carol}, XRP(10'000));
env.trust(USD(1'200), bob, carol);
env(pay(gw, bob, USD(1'200)));
AMM ammBob(env, bob, XRP(5'500), USD(1'200));
env(pay(alice, carol, USD(10'000)),
paths(XRP),
delivermin(USD(200)),
txflags(tfPartialPayment),
sendmax(XRP(1'000)),
ter(tecPATH_PARTIAL));
env(pay(alice, carol, USD(10'000)),
paths(XRP),
delivermin(USD(200)),
txflags(tfPartialPayment),
sendmax(XRP(1'100)));
BEAST_EXPECT(
ammBob.expectBalances(XRP(6'600), USD(1'000), ammBob.tokens()));
env.require(balance(carol, USD(200)));
}
{
auto const dan = Account("dan");
Env env(*this, features);
fund(env, gw, {alice, bob, carol, dan}, XRP(10'000));
env.close();
env.trust(USD(1'100), bob, carol, dan);
env(pay(gw, bob, USD(100)));
env(pay(gw, dan, USD(1'100)));
env(offer(bob, XRP(100), USD(100)));
env(offer(bob, XRP(1'000), USD(100)));
AMM ammDan(env, dan, XRP(1'000), USD(1'100));
if (!features[fixAMMv1_1])
{
env(pay(alice, carol, USD(10'000)),
paths(XRP),
delivermin(USD(200)),
txflags(tfPartialPayment),
sendmax(XRP(200)));
env.require(balance(bob, USD(0)));
env.require(balance(carol, USD(200)));
BEAST_EXPECT(ammDan.expectBalances(
XRP(1'100), USD(1'000), ammDan.tokens()));
}
else
{
env(pay(alice, carol, USD(10'000)),
paths(XRP),
delivermin(USD(200)),
txflags(tfPartialPayment),
sendmax(XRPAmount(200'000'001)));
env.require(balance(bob, USD(0)));
env.require(balance(
carol, STAmount{USD, UINT64_C(200'00000090909), -11}));
BEAST_EXPECT(ammDan.expectBalances(
XRPAmount{1'100'000'001},
STAmount{USD, UINT64_C(999'99999909091), -11},
ammDan.tokens()));
}
}
}
void
testPayment(FeatureBitset features)
{
testcase("Payment");
using namespace jtx;
Account const becky{"becky"};
bool const supportsPreauth = {features[featureDepositPreauth]};
// The initial implementation of DepositAuth had a bug where an
// account with the DepositAuth flag set could not make a payment
// to itself. That bug was fixed in the DepositPreauth amendment.
Env env(*this, features);
fund(env, gw, {alice, becky}, XRP(5'000));
env.close();
env.trust(USD(1'000), alice);
env.trust(USD(1'000), becky);
env.close();
env(pay(gw, alice, USD(500)));
env.close();
AMM ammAlice(env, alice, XRP(100), USD(140));
// becky pays herself USD (10) by consuming part of alice's offer.
// Make sure the payment works if PaymentAuth is not involved.
env(pay(becky, becky, USD(10)), path(~USD), sendmax(XRP(10)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(107'692'308), USD(130), ammAlice.tokens()));
// becky decides to require authorization for deposits.
env(fset(becky, asfDepositAuth));
env.close();
// becky pays herself again. Whether it succeeds depends on
// whether featureDepositPreauth is enabled.
TER const expect{
supportsPreauth ? TER{tesSUCCESS} : TER{tecNO_PERMISSION}};
env(pay(becky, becky, USD(10)),
path(~USD),
sendmax(XRP(10)),
ter(expect));
env.close();
}
void
testPayIOU()
{
// Exercise IOU payments and non-direct XRP payments to an account
// that has the lsfDepositAuth flag set.
testcase("Pay IOU");
using namespace jtx;
Env env(*this);
fund(env, gw, {alice, bob, carol}, XRP(10'000));
env.trust(USD(1'000), alice, bob, carol);
env.close();
env(pay(gw, alice, USD(150)));
env(pay(gw, carol, USD(150)));
AMM ammCarol(env, carol, USD(100), XRPAmount(101));
// Make sure bob's trust line is all set up so he can receive USD.
env(pay(alice, bob, USD(50)));
env.close();
// bob sets the lsfDepositAuth flag.
env(fset(bob, asfDepositAuth), require(flags(bob, asfDepositAuth)));
env.close();
// None of the following payments should succeed.
auto failedIouPayments = [this, &env]() {
env.require(flags(bob, asfDepositAuth));
// Capture bob's balances before hand to confirm they don't
// change.
PrettyAmount const bobXrpBalance{env.balance(bob, XRP)};
PrettyAmount const bobUsdBalance{env.balance(bob, USD)};
env(pay(alice, bob, USD(50)), ter(tecNO_PERMISSION));
env.close();
// Note that even though alice is paying bob in XRP, the payment
// is still not allowed since the payment passes through an
// offer.
env(pay(alice, bob, drops(1)),
sendmax(USD(1)),
ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(bobXrpBalance == env.balance(bob, XRP));
BEAST_EXPECT(bobUsdBalance == env.balance(bob, USD));
};
// Test when bob has an XRP balance > base reserve.
failedIouPayments();
// Set bob's XRP balance == base reserve. Also demonstrate that
// bob can make payments while his lsfDepositAuth flag is set.
env(pay(bob, alice, USD(25)));
env.close();
{
STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)};
XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
env(pay(bob, alice, bobPaysXRP), fee(bobPaysFee));
env.close();
}
// Test when bob's XRP balance == base reserve.
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
BEAST_EXPECT(env.balance(bob, USD) == USD(25));
failedIouPayments();
// Test when bob has an XRP balance == 0.
env(noop(bob), fee(reserve(env, 0)));
env.close();
BEAST_EXPECT(env.balance(bob, XRP) == XRP(0));
failedIouPayments();
// Give bob enough XRP for the fee to clear the lsfDepositAuth flag.
env(pay(alice, bob, drops(env.current()->fees().base)));
// bob clears the lsfDepositAuth and the next payment succeeds.
env(fclear(bob, asfDepositAuth));
env.close();
env(pay(alice, bob, USD(50)));
env.close();
env(pay(alice, bob, drops(1)), sendmax(USD(1)));
env.close();
BEAST_EXPECT(ammCarol.expectBalances(
USD(101), XRPAmount(100), ammCarol.tokens()));
}
void
testRippleState(FeatureBitset features)
{
testcase("RippleState Freeze");
using namespace test::jtx;
Env env(*this, features);
Account const G1{"G1"};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(1'000), G1, alice, bob);
env.close();
env.trust(G1["USD"](100), bob);
env.trust(G1["USD"](205), alice);
env.close();
env(pay(G1, bob, G1["USD"](10)));
env(pay(G1, alice, G1["USD"](205)));
env.close();
AMM ammAlice(env, alice, XRP(500), G1["USD"](105));
{
auto lines = getAccountLines(env, bob);
if (!BEAST_EXPECT(checkArraySize(lines[jss::lines], 1u)))
return;
BEAST_EXPECT(lines[jss::lines][0u][jss::account] == G1.human());
BEAST_EXPECT(lines[jss::lines][0u][jss::limit] == "100");
BEAST_EXPECT(lines[jss::lines][0u][jss::balance] == "10");
}
{
auto lines = getAccountLines(env, alice, G1["USD"]);
if (!BEAST_EXPECT(checkArraySize(lines[jss::lines], 1u)))
return;
BEAST_EXPECT(lines[jss::lines][0u][jss::account] == G1.human());
BEAST_EXPECT(lines[jss::lines][0u][jss::limit] == "205");
// 105 transferred to AMM
BEAST_EXPECT(lines[jss::lines][0u][jss::balance] == "100");
}
// Account with line unfrozen (proving operations normally work)
// test: can make Payment on that line
env(pay(alice, bob, G1["USD"](1)));
// test: can receive Payment on that line
env(pay(bob, alice, G1["USD"](1)));
env.close();
// Is created via a TrustSet with SetFreeze flag
// test: sets LowFreeze | HighFreeze flags
env(trust(G1, bob["USD"](0), tfSetFreeze));
env.close();
{
// Account with line frozen by issuer
// test: can buy more assets on that line
env(offer(bob, G1["USD"](5), XRP(25)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(525), G1["USD"](100), ammAlice.tokens()));
}
{
// test: can not sell assets from that line
env(offer(bob, XRP(1), G1["USD"](5)), ter(tecUNFUNDED_OFFER));
// test: can receive Payment on that line
env(pay(alice, bob, G1["USD"](1)));
// test: can not make Payment from that line
env(pay(bob, alice, G1["USD"](1)), ter(tecPATH_DRY));
}
{
// check G1 account lines
// test: shows freeze
auto lines = getAccountLines(env, G1);
Json::Value bobLine;
for (auto const& it : lines[jss::lines])
{
if (it[jss::account] == bob.human())
{
bobLine = it;
break;
}
}
if (!BEAST_EXPECT(bobLine))
return;
BEAST_EXPECT(bobLine[jss::freeze] == true);
BEAST_EXPECT(bobLine[jss::balance] == "-16");
}
{
// test: shows freeze peer
auto lines = getAccountLines(env, bob);
Json::Value g1Line;
for (auto const& it : lines[jss::lines])
{
if (it[jss::account] == G1.human())
{
g1Line = it;
break;
}
}
if (!BEAST_EXPECT(g1Line))
return;
BEAST_EXPECT(g1Line[jss::freeze_peer] == true);
BEAST_EXPECT(g1Line[jss::balance] == "16");
}
{
// Is cleared via a TrustSet with ClearFreeze flag
// test: sets LowFreeze | HighFreeze flags
env(trust(G1, bob["USD"](0), tfClearFreeze));
auto affected = env.meta()->getJson(
JsonOptions::none)[sfAffectedNodes.fieldName];
if (!BEAST_EXPECT(checkArraySize(affected, 2u)))
return;
auto ff =
affected[1u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
BEAST_EXPECT(
ff[sfLowLimit.fieldName] ==
G1["USD"](0).value().getJson(JsonOptions::none));
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfLowFreeze));
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfHighFreeze));
env.close();
}
}
void
testGlobalFreeze(FeatureBitset features)
{
testcase("Global Freeze");
using namespace test::jtx;
Env env(*this, features);
Account G1{"G1"};
Account A1{"A1"};
Account A2{"A2"};
Account A3{"A3"};
Account A4{"A4"};
env.fund(XRP(12'000), G1);
env.fund(XRP(1'000), A1);
env.fund(XRP(20'000), A2, A3, A4);
env.close();
env.trust(G1["USD"](1'200), A1);
env.trust(G1["USD"](200), A2);
env.trust(G1["BTC"](100), A3);
env.trust(G1["BTC"](100), A4);
env.close();
env(pay(G1, A1, G1["USD"](1'000)));
env(pay(G1, A2, G1["USD"](100)));
env(pay(G1, A3, G1["BTC"](100)));
env(pay(G1, A4, G1["BTC"](100)));
env.close();
AMM ammG1(env, G1, XRP(10'000), G1["USD"](100));
env(offer(A1, XRP(10'000), G1["USD"](100)), txflags(tfPassive));
env(offer(A2, G1["USD"](100), XRP(10'000)), txflags(tfPassive));
env.close();
{
// Account without GlobalFreeze (proving operations normally
// work)
// test: visible offers where taker_pays is unfrozen issuer
auto offers = env.rpc(
"book_offers",
std::string("USD/") + G1.human(),
"XRP")[jss::result][jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
std::set<std::string> accounts;
for (auto const& offer : offers)
{
accounts.insert(offer[jss::Account].asString());
}
BEAST_EXPECT(accounts.find(A2.human()) != std::end(accounts));
// test: visible offers where taker_gets is unfrozen issuer
offers = env.rpc(
"book_offers",
"XRP",
std::string("USD/") + G1.human())[jss::result][jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
accounts.clear();
for (auto const& offer : offers)
{
accounts.insert(offer[jss::Account].asString());
}
BEAST_EXPECT(accounts.find(A1.human()) != std::end(accounts));
}
{
// Offers/Payments
// test: assets can be bought on the market
// env(offer(A3, G1["BTC"](1), XRP(1)));
AMM ammA3(env, A3, G1["BTC"](1), XRP(1));
// test: assets can be sold on the market
// AMM is bidirectional
// test: direct issues can be sent
env(pay(G1, A2, G1["USD"](1)));
// test: direct redemptions can be sent
env(pay(A2, G1, G1["USD"](1)));
// test: via rippling can be sent
env(pay(A2, A1, G1["USD"](1)));
// test: via rippling can be sent back
env(pay(A1, A2, G1["USD"](1)));
ammA3.withdrawAll(std::nullopt);
}
{
// Account with GlobalFreeze
// set GlobalFreeze first
// test: SetFlag GlobalFreeze will toggle back to freeze
env.require(nflags(G1, asfGlobalFreeze));
env(fset(G1, asfGlobalFreeze));
env.require(flags(G1, asfGlobalFreeze));
env.require(nflags(G1, asfNoFreeze));
// test: assets can't be bought on the market
AMM ammA3(env, A3, G1["BTC"](1), XRP(1), ter(tecFROZEN));
// test: assets can't be sold on the market
// AMM is bidirectional
}
{
// test: book_offers shows offers
// (should these actually be filtered?)
auto offers = env.rpc(
"book_offers",
"XRP",
std::string("USD/") + G1.human())[jss::result][jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
offers = env.rpc(
"book_offers",
std::string("USD/") + G1.human(),
"XRP")[jss::result][jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
}
{
// Payments
// test: direct issues can be sent
env(pay(G1, A2, G1["USD"](1)));
// test: direct redemptions can be sent
env(pay(A2, G1, G1["USD"](1)));
// test: via rippling cant be sent
env(pay(A2, A1, G1["USD"](1)), ter(tecPATH_DRY));
}
}
void
testOffersWhenFrozen(FeatureBitset features)
{
testcase("Offers for Frozen Trust Lines");
using namespace test::jtx;
Env env(*this, features);
Account G1{"G1"};
Account A2{"A2"};
Account A3{"A3"};
Account A4{"A4"};
env.fund(XRP(2'000), G1, A3, A4);
env.fund(XRP(2'000), A2);
env.close();
env.trust(G1["USD"](1'000), A2);
env.trust(G1["USD"](2'000), A3);
env.trust(G1["USD"](2'001), A4);
env.close();
env(pay(G1, A3, G1["USD"](2'000)));
env(pay(G1, A4, G1["USD"](2'001)));
env.close();
AMM ammA3(env, A3, XRP(1'000), G1["USD"](1'001));
// removal after successful payment
// test: make a payment with partially consuming offer
env(pay(A2, G1, G1["USD"](1)), paths(G1["USD"]), sendmax(XRP(1)));
env.close();
BEAST_EXPECT(
ammA3.expectBalances(XRP(1'001), G1["USD"](1'000), ammA3.tokens()));
// test: someone else creates an offer providing liquidity
env(offer(A4, XRP(999), G1["USD"](999)));
env.close();
// The offer consumes AMM offer
BEAST_EXPECT(
ammA3.expectBalances(XRP(1'000), G1["USD"](1'001), ammA3.tokens()));
// test: AMM line is frozen
auto const a3am =
STAmount{Issue{to_currency("USD"), ammA3.ammAccount()}, 0};
env(trust(G1, a3am, tfSetFreeze));
auto const info = ammA3.ammRpcInfo();
BEAST_EXPECT(info[jss::amm][jss::asset2_frozen].asBool());
env.close();
// test: Can make a payment via the new offer
env(pay(A2, G1, G1["USD"](1)), paths(G1["USD"]), sendmax(XRP(1)));
env.close();
// AMM is not consumed
BEAST_EXPECT(
ammA3.expectBalances(XRP(1'000), G1["USD"](1'001), ammA3.tokens()));
// removal buy successful OfferCreate
// test: freeze the new offer
env(trust(G1, A4["USD"](0), tfSetFreeze));
env.close();
// test: can no longer create a crossing offer
env(offer(A2, G1["USD"](999), XRP(999)));
env.close();
// test: offer was removed by offer_create
auto offers = getAccountOffers(env, A4)[jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 0u)))
return;
}
void
testTxMultisign(FeatureBitset features)
{
testcase("Multisign AMM Transactions");
using namespace jtx;
Env env{*this, features};
Account const bogie{"bogie", KeyType::secp256k1};
Account const alice{"alice", KeyType::secp256k1};
Account const becky{"becky", KeyType::ed25519};
Account const zelda{"zelda", KeyType::secp256k1};
fund(env, gw, {alice, becky, zelda}, XRP(20'000), {USD(20'000)});
// alice uses a regular key with the master disabled.
Account const alie{"alie", KeyType::secp256k1};
env(regkey(alice, alie));
env(fset(alice, asfDisableMaster), sig(alice));
// Attach signers to alice.
env(signers(alice, 2, {{becky, 1}, {bogie, 1}}), sig(alie));
env.close();
int const signerListOwners{features[featureMultiSignReserve] ? 2 : 5};
env.require(owners(alice, signerListOwners + 0));
msig const ms{becky, bogie};
// Multisign all AMM transactions
AMM ammAlice(
env,
alice,
XRP(10'000),
USD(10'000),
false,
0,
ammCrtFee(env).drops(),
std::nullopt,
std::nullopt,
ms,
ter(tesSUCCESS));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
ammAlice.deposit(alice, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
ammAlice.withdraw(alice, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
ammAlice.vote({}, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
env(ammAlice.bid({.account = alice, .bidMin = 100}), ms).close();
BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{4'000}));
// 4000 tokens burnt
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{9'996'000, 0}));
}
void
testToStrand(FeatureBitset features)
{
testcase("To Strand");
using namespace jtx;
// cannot have more than one offer with the same output issue
Env env(*this, features);
fund(
env,
gw,
{alice, bob, carol},
XRP(10'000),
{USD(2'000), EUR(1'000)});
AMM bobXRP_USD(env, bob, XRP(1'000), USD(1'000));
AMM bobUSD_EUR(env, bob, USD(1'000), EUR(1'000));
// payment path: XRP -> XRP/USD -> USD/EUR -> EUR/USD
env(pay(alice, carol, USD(100)),
path(~USD, ~EUR, ~USD),
sendmax(XRP(200)),
txflags(tfNoRippleDirect),
ter(temBAD_PATH_LOOP));
}
void
testRIPD1373(FeatureBitset features)
{
using namespace jtx;
testcase("RIPD1373");
{
Env env(*this, features);
auto const BobUSD = bob["USD"];
auto const BobEUR = bob["EUR"];
fund(env, gw, {alice, bob}, XRP(10'000));
env.trust(USD(1'000), alice, bob);
env.trust(EUR(1'000), alice, bob);
env.close();
fund(
env,
bob,
{alice, gw},
{BobUSD(100), BobEUR(100)},
Fund::IOUOnly);
env.close();
AMM ammBobXRP_USD(env, bob, XRP(100), BobUSD(100));
env(offer(gw, XRP(100), USD(100)), txflags(tfPassive));
AMM ammBobUSD_EUR(env, bob, BobUSD(100), BobEUR(100));
env(offer(gw, USD(100), EUR(100)), txflags(tfPassive));
Path const p = [&] {
Path result;
result.push_back(allpe(gw, BobUSD));
result.push_back(cpe(EUR.currency));
return result;
}();
PathSet paths(p);
env(pay(alice, alice, EUR(1)),
json(paths.json()),
sendmax(XRP(10)),
txflags(tfNoRippleDirect | tfPartialPayment),
ter(temBAD_PATH));
}
{
Env env(*this, features);
fund(env, gw, {alice, bob, carol}, XRP(10'000), {USD(100)});
AMM ammBob(env, bob, XRP(100), USD(100));
// payment path: XRP -> XRP/USD -> USD/XRP
env(pay(alice, carol, XRP(100)),
path(~USD, ~XRP),
txflags(tfNoRippleDirect),
ter(temBAD_SEND_XRP_PATHS));
}
{
Env env(*this, features);
fund(env, gw, {alice, bob, carol}, XRP(10'000), {USD(100)});
AMM ammBob(env, bob, XRP(100), USD(100));
// payment path: XRP -> XRP/USD -> USD/XRP
env(pay(alice, carol, XRP(100)),
path(~USD, ~XRP),
sendmax(XRP(200)),
txflags(tfNoRippleDirect),
ter(temBAD_SEND_XRP_MAX));
}
}
void
testLoop(FeatureBitset features)
{
testcase("test loop");
using namespace jtx;
auto const CNY = gw["CNY"];
{
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
env.close();
env.trust(USD(10'000), alice, bob, carol);
env.close();
env(pay(gw, bob, USD(100)));
env(pay(gw, alice, USD(100)));
env.close();
AMM ammBob(env, bob, XRP(100), USD(100));
// payment path: USD -> USD/XRP -> XRP/USD
env(pay(alice, carol, USD(100)),
sendmax(USD(100)),
path(~XRP, ~USD),
txflags(tfNoRippleDirect),
ter(temBAD_PATH_LOOP));
}
{
Env env(*this, features);
env.fund(XRP(10'000), alice, bob, carol, gw);
env.close();
env.trust(USD(10'000), alice, bob, carol);
env.trust(EUR(10'000), alice, bob, carol);
env.trust(CNY(10'000), alice, bob, carol);
env(pay(gw, bob, USD(200)));
env(pay(gw, bob, EUR(200)));
env(pay(gw, bob, CNY(100)));
AMM ammBobXRP_USD(env, bob, XRP(100), USD(100));
AMM ammBobUSD_EUR(env, bob, USD(100), EUR(100));
AMM ammBobEUR_CNY(env, bob, EUR(100), CNY(100));
// payment path: XRP->XRP/USD->USD/EUR->USD/CNY
env(pay(alice, carol, CNY(100)),
sendmax(XRP(100)),
path(~USD, ~EUR, ~USD, ~CNY),
txflags(tfNoRippleDirect),
ter(temBAD_PATH_LOOP));
}
}
void
testPaths()
{
path_find_consume_all();
via_offers_via_gateway();
receive_max();
path_find_01();
path_find_02();
path_find_05();
path_find_06();
}
void
testFlow()
{
using namespace jtx;
FeatureBitset const all{testable_amendments()};
testFalseDry(all);
testBookStep(all);
testTransferRateNoOwnerFee(all);
testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3);
testLimitQuality();
testXRPPathLoop();
}
void
testCrossingLimits()
{
using namespace jtx;
FeatureBitset const all{testable_amendments()};
testStepLimit(all);
testStepLimit(all - fixAMMv1_1 - fixAMMv1_3);
}
void
testDeliverMin()
{
using namespace jtx;
FeatureBitset const all{testable_amendments()};
test_convert_all_of_an_asset(all);
test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3);
}
void
testDepositAuth()
{
auto const supported{jtx::testable_amendments()};
testPayment(supported - featureDepositPreauth);
testPayment(supported);
testPayIOU();
}
void
testFreeze()
{
using namespace test::jtx;
auto const sa = testable_amendments();
testRippleState(sa);
testGlobalFreeze(sa);
testOffersWhenFrozen(sa);
}
void
testMultisign()
{
using namespace jtx;
auto const all = testable_amendments();
testTxMultisign(
all - featureMultiSignReserve - featureExpandedSignerList);
testTxMultisign(all - featureExpandedSignerList);
testTxMultisign(all);
}
void
testPayStrand()
{
using namespace jtx;
auto const all = testable_amendments();
testToStrand(all);
testRIPD1373(all);
testLoop(all);
}
void
run() override
{
testOffers();
testPaths();
testFlow();
testCrossingLimits();
testDeliverMin();
testDepositAuth();
testFreeze();
testMultisign();
testPayStrand();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(AMMExtended, app, ripple, 1);
} // namespace test
} // namespace ripple