Files
rippled/src/test/app/AMMExtended_test.cpp
Vito Tumas 5865bd017f refactor: Update transaction folder structure (#6483)
This change reorganizes the `tx/transactors` directory for consistency and discoverability. There are no behavioral changes, this is a pure refactor. Underscores were chosen as the way to separate multi-words as this is the more popular option in C++ projects.
 
Specific changes:
- Rename all subdirectories to lowercase/snake_case (`AMM` → `amm`, `Check` → `check`, `NFT` → `nft`, `PermissionedDomain` → `permissioned_domain`, etc.)
- Merge `AMM/` and `Offer/` into `dex/`, including `PermissionedDEXHelpers`
- Rename `MPT/` → `token/`, absorbing `SetTrust` and `Clawback`
- Move top-level transactors into named groups: `account/`, `bridge/`, `credentials/`, `did/`, `escrow/`, `oracle/`, `payment/`, `payment_channel/`, `system/`
- Update all include paths across the codebase and `transactions.macro`
2026-03-06 08:25:31 +00:00

3544 lines
129 KiB
C++

#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/paths/AMMOffer.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/tx/paths/Flow.h>
#include <xrpl/tx/paths/detail/StrandFlow.h>
#include <xrpl/tx/transactors/dex/AMMContext.h>
#include <xrpl/tx/transactors/dex/AMMUtils.h>
#include <utility>
#include <vector>
namespace xrpl {
namespace test {
/**
* Tests of AMM that use offers too.
*/
struct AMMExtended_test : public jtx::AMMTest
{
// Use small Number mantissas for the life of this test.
NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small};
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;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas
FeatureBitset const all{
testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
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)), deliver_min(USD(10)), ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
deliver_min(USD(-5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
deliver_min(XRP(5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
deliver_min(Account(carol)["USD"](5)),
txflags(tfPartialPayment),
ter(temBAD_AMOUNT));
env(pay(alice, bob, USD(10)),
deliver_min(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),
deliver_min(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),
deliver_min(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),
deliver_min(USD(200)),
txflags(tfPartialPayment),
sendmax(XRP(1'000)),
ter(tecPATH_PARTIAL));
env(pay(alice, carol, USD(10'000)),
paths(XRP),
deliver_min(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),
deliver_min(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),
deliver_min(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"};
// 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.
env(pay(becky, becky, USD(10)), path(~USD), sendmax(XRP(10)), ter(tesSUCCESS));
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();
env.require(owners(alice, 2));
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(allPathElements(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;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
testFalseDry(all);
testBookStep(all);
testTransferRateNoOwnerFee(all);
testTransferRateNoOwnerFee(all - fixAMMv1_1 - fixAMMv1_3);
testLimitQuality();
testXRPPathLoop();
}
void
testCrossingLimits()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
testStepLimit(all);
testStepLimit(all - fixAMMv1_1 - fixAMMv1_3);
}
void
testDeliverMin()
{
using namespace jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
test_convert_all_of_an_asset(all);
test_convert_all_of_an_asset(all - fixAMMv1_1 - fixAMMv1_3);
}
void
testDepositAuth()
{
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const all{
jtx::testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
testPayment(all);
testPayIOU();
}
void
testFreeze()
{
using namespace test::jtx;
// For now, just disable SAV entirely, which locks in the small Number
// mantissas in the transaction engine
FeatureBitset const sa{
testable_amendments() - featureSingleAssetVault - featureLendingProtocol};
testRippleState(sa);
testGlobalFreeze(sa);
testOffersWhenFrozen(sa);
}
void
testMultisign()
{
testTxMultisign(jtx::testable_amendments());
}
void
testPayStrand()
{
auto const all = jtx::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, xrpl, 1);
} // namespace test
} // namespace xrpl