Files
rippled/src/test/app/AMMExtendedMPT_test.cpp
2026-05-15 15:32:19 +00:00

3673 lines
133 KiB
C++

#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/Env.h>
#include <test/jtx/PathSet.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/balance.h>
#include <test/jtx/delivermin.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/jtx_json.h>
#include <test/jtx/mpt.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/offer.h>
#include <test/jtx/owners.h>
#include <test/jtx/paths.h>
#include <test/jtx/pay.h>
#include <test/jtx/regkey.h>
#include <test/jtx/require.h>
#include <test/jtx/sendmax.h>
#include <test/jtx/sig.h>
#include <test/jtx/tags.h>
#include <test/jtx/ter.h>
#include <test/jtx/txflags.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/OfferHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STPathSet.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/tx/paths/Flow.h>
#include <xrpl/tx/paths/detail/Steps.h>
#include <cstdint>
#include <optional>
#include <tuple>
namespace xrpl::test {
/**
* Tests of AMM MPT that use offers.
*/
struct AMMExtendedMPT_test : public jtx::AMMTest
{
private:
void
testRmFundedOffer(FeatureBitset features)
{
testcase("Incorrect Removal of Funded Offers");
// We need at least two paths. One at good quality and one at bad
// quality. The bad quality path needs two offer books in a row.
// Each offer book should have two offers at the same quality, the
// offers should be completely consumed, and the payment should
// require both offers to be satisfied. The first offer must
// be "taker gets" XRP. Ensure that the payment engine does not remove
// the first "taker gets" xrp offer, because the offer is still
// funded and not used for the payment.
using namespace jtx;
Env env{*this, features};
fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 200'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 2'000'000'000'000'000,
.flags = kMptDexFlags});
// 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'000'000'000'000), XRP(49)));
env(offer(carol_, btc(51'000'000'000'000), XRP(51)));
// Offers for the poor quality path
// Must be two offers at the same quality
env(offer(carol_, XRP(50), eth(50'000'000'000'000)));
env(offer(carol_, XRP(50), eth(50'000'000'000'000)));
// Good quality path
AMM const ammCarol(env, carol_, btc(1'000'000'000'000'000), eth(100'100'000'000'000'000));
PathSet const paths(TestPath(XRP, MPT(eth)), TestPath(MPT(eth)));
env(pay(alice_, bob_, eth(100'000'000'000'000)),
Json(paths.json()),
Sendmax(btc(1'000'000'000'000'000)),
Txflags(tfPartialPayment));
BEAST_EXPECT(ammCarol.expectBalances(
btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
env.require(Balance(bob_, eth(200'100'000'000'000'000)));
BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
}
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) {
auto const& btc = MPT(ammAlice[1]);
auto const baseFee = env.current()->fees().base;
auto carolBTC = env.balance(carol_, btc);
auto carolXRP = env.balance(carol_, XRP);
// Order that can't be filled
env(offer(carol_, btc(100), XRP(100)), Txflags(tfFillOrKill), Ter(tecKILLED));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens()));
// fee = AMM
env.require(Balance(carol_, carolXRP - baseFee));
env.require(Balance(carol_, carolBTC));
BEAST_EXPECT(expectOffers(env, carol_, 0));
carolXRP = env.balance(carol_, XRP);
// Order that can be filled
env(offer(carol_, XRP(100), btc(100)), Txflags(tfFillOrKill), Ter(tesSUCCESS));
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens()));
env.require(Balance(carol_, carolXRP + XRP(100) - baseFee));
env.require(Balance(carol_, carolBTC - btc(100)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
},
{{XRP(10'100), gAmmmpt(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) {
auto const& btc = MPT(ammAlice[1]);
auto const baseFee = env.current()->fees().base;
auto carolBTC = env.balance(carol_, btc);
auto carolXRP = env.balance(carol_, XRP);
env(offer(carol_, XRP(200), btc(200)),
Txflags(tfImmediateOrCancel),
Ter(tesSUCCESS));
// AMM generates a synthetic offer of 100BTC/100XRP
// to match the CLOB offer quality.
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens()));
// +AMM - offer * fee
env.require(Balance(carol_, carolXRP + XRP(100) - baseFee));
env.require(Balance(carol_, carolBTC - btc(100)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
},
{{XRP(10'100), gAmmmpt(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.
auto const& btc = MPT(ammAlice[1]);
env(offer(carol_, XRP(100), btc(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), btc(100)}}}));
},
{{XRP(10'100), gAmmmpt(10'000)}},
0,
std::nullopt,
{features});
// tfPassive -- cross only offers of better quality.
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const& btc = MPT(ammAlice[1]);
env(offer(alice_, btc(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), btc(100), tfPassive));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'900), btc(9083), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 0));
BEAST_EXPECT(expectOffers(env, alice_, 1));
},
{{XRP(11'000), gAmmmpt(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));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 100'000'000,
.flags = kMptDexFlags});
AMM const ammAlice(env, alice_, XRP(150'000), btc(50'000'000));
// Existing offer pays better than this wants.
// Partially consume existing offer.
// Pay 1'000'000 BTC, get 3061224490 Drops.
auto const xrpTransferred = XRPAmount{3'061'224'490};
env(offer(bob_, btc(1'000'000), XRP(4'000)));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(150'000) + xrpTransferred, btc(49'000'000), IOUAmount{273'861'278752583, -5}));
env.require(Balance(bob_, btc(101'000'000)));
BEAST_EXPECT(
expectLedgerEntryRoot(env, bob_, XRP(300'000) - xrpTransferred - 2 * 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();
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, alice_, btc(500'000'000)));
AMM const ammAlice(env, alice_, XRP(150'000), btc(51'000'000));
env(offer(bob_, btc(1'000'000), XRP(3'000)));
BEAST_EXPECT(ammAlice.expectBalances(XRP(153'000), btc(50'000'000), ammAlice.tokens()));
env.require(Balance(bob_, btc(1'000'000)));
env.require(Balance(bob_, XRP(200'000) - XRP(3'000) - env.current()->fees().base * 2));
}
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));
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(1'000'000'000)));
env.require(Owners(alice_, 1), Owners(bob_, 1));
env(pay(gw_, alice_, btc(100'000'000)));
AMM const ammBob(env, bob_, btc(200'000'000), XRP(1'500));
env(pay(alice_, alice_, XRP(500)), Sendmax(btc(100'000'000)));
BEAST_EXPECT(ammBob.expectBalances(btc(300'000'000), XRP(1'000), ammBob.tokens()));
env.require(Balance(alice_, btc(0)));
auto jrr = ledgerEntryRoot(env, alice_);
env.require(Balance(alice_, XRP(10'000) + XRP(500) - env.current()->fees().base * 2));
}
void
testCurrencyConversionInParts(FeatureBitset features)
{
testcase("Currency Conversion: In Parts");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(30'000), gw_, bob_);
env.fund(XRP(40'000), alice_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 30'000'000'000,
.flags = kMptDexFlags});
env(pay(gw_, alice_, btc(10'000'000'000)));
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000));
env.close();
// Alice converts BTC to XRP which should fail
// due to PartialPayment.
env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Ter(tecPATH_PARTIAL));
// Alice converts BTC to XRP, should succeed because
// we permit partial payment
env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{9'900'990'100}, btc(10'100'000'000), ammAlice.tokens()));
// initial 40,000'000'000 - 10,000'000'000AMM - 100'000'000pay
env.require(Balance(alice_, btc(29'900'000'000)));
// initial 40,000 - 10,0000AMM + 99.009900pay - fee*3
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice_,
XRP(40'000) - XRP(10'000) + XRPAmount{99'009'900} - ammCrtFee(env) - txFee(env, 3)));
}
void
testCrossCurrencyStartXRP(FeatureBitset features)
{
testcase("Cross Currency Payment: Start with XRP");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(30'000), gw_);
env.fund(XRP(40'000), alice_);
env.fund(XRP(1'000), bob_);
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, alice_, btc(10'100'000'000)));
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
env.close();
env(pay(alice_, bob_, btc(100'000'000)), Sendmax(XRP(100)));
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
env.require(Balance(bob_, btc(100'000'000)));
}
void
testCrossCurrencyEndXRP(FeatureBitset features)
{
testcase("Cross Currency Payment: End with XRP");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(30'000), gw_);
env.fund(XRP(40'100), alice_);
env.fund(XRP(1'000), bob_);
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, alice_, btc(40'000'000'000)));
AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000'000));
env.close();
env(pay(alice_, bob_, XRP(100)), Sendmax(btc(100'000'000)));
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000'000), ammAlice.tokens()));
BEAST_EXPECT(expectLedgerEntryRoot(env, bob_, XRP(1'000) + XRP(100) - txFee(env, 1)));
}
void
testCrossCurrencyBridged(FeatureBitset features)
{
testcase("Cross Currency Payment: Bridged");
using namespace jtx;
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
auto const dan = Account{"dan"};
env.fund(XRP(60'000), alice_, bob_, carol_, gw_, dan);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_, carol_, dan},
.limit = 10'000'000'000'000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_, dan},
.limit = 10'000'000'000'000'000});
env(pay(gw_, alice_, btc(500'000'000'000'000)));
env(pay(gw_, carol_, btc(6'000'000'000'000'000)));
env(pay(gw_, dan, eth(400'000'000'000'000)));
env.close();
env.close();
AMM const ammCarol(env, carol_, btc(5'000'000'000'000'000), XRP(50'000));
env(offer(dan, XRP(500), eth(50'000'000'000'000)));
env.close();
json::Value jtp{json::ValueType::Array};
jtp[0u][0u][jss::currency] = "XRP";
env(pay(alice_, bob_, eth(30'000'000'000'000)),
Json(jss::Paths, jtp),
Sendmax(btc(333'000'000'000'000)));
env.close();
BEAST_EXPECT(ammCarol.expectBalances(
XRP(49'700), btc(5'030'181'086'519'115), ammCarol.tokens()));
BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), eth(20'000'000'000'000)}}}));
env.require(Balance(bob_, eth(30'000'000'000'000)));
};
testHelper2TokensMix(test);
}
void
testOfferFeesConsumeFunds(FeatureBitset features)
{
testcase("Offer Fees Consume Funds");
using namespace jtx;
Env env{*this, features};
// Provide micro amounts to compensate for fees to make results round
// nice.
auto const startingXrp =
XRP(100) + env.current()->fees().accountReserve(2) + env.current()->fees().base * 3;
env.fund(startingXrp, gw_, alice_);
env.fund(XRP(2'000), bob_);
env.close();
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
// Created only to increase one reserve count for alice
MPTTester const eth(
{.env = env, .issuer = gw_, .holders = {alice_}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(1'200'000'000'000'000)));
AMM const ammBob(env, bob_, XRP(1'000), btc(1'200'000'000'000'000));
// Alice has 400 - (2 reserve of 50 = 300 reserve) = 100 available.
// Ask for more than available to prove reserve works.
env(offer(alice_, btc(200'000'000'000'000), XRP(200)));
// The pool gets only 100XRP for ~109.09e12BTC, even though
// it can exchange more.
BEAST_EXPECT(
ammBob.expectBalances(XRP(1'100), btc(1'090'909'090'909'091), ammBob.tokens()));
env.require(Balance(alice_, btc(109'090'909'090'909)));
env.require(Balance(alice_, XRP(300)));
}
void
testOfferCreateThenCross(FeatureBitset features)
{
testcase("Offer Create, then Cross");
using namespace jtx;
Env env{*this, features};
fund(env, gw_, {alice_, bob_}, XRP(200'000));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.transferFee = 500,
.flags = kMptDexFlags});
env(pay(gw_, bob_, btc(1'000'000'000'000)));
env(pay(gw_, alice_, btc(200'000'000'000'000)));
AMM const ammAlice(env, alice_, btc(150'000'000'000'000), XRP(150'100));
env(offer(bob_, XRP(100), btc(100'000'000'000)));
BEAST_EXPECT(
ammAlice.expectBalances(btc(150'100'000'000'000), XRP(150'000), ammAlice.tokens()));
// Bob pays 0.005 transfer fee.
env.require(Balance(bob_, btc(899'500'000'000)));
}
void
testSellFlagBasic(FeatureBitset features)
{
testcase("Offer tfSell: Basic Sell");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(30'000), gw_, bob_, carol_);
env.fund(XRP(39'900), alice_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 30'000,
.flags = kMptDexFlags});
env(pay(gw_, alice_, btc(10'100)));
AMM const ammAlice(env, alice_, XRP(9'900), btc(10'100));
env(offer(carol_, btc(100), XRP(100)), Json(jss::Flags, tfSell));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(9'999), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol_, 0));
env.require(Balance(carol_, btc(30'101)));
BEAST_EXPECT(
expectLedgerEntryRoot(env, carol_, XRP(30'000) - XRP(100) - 2 * txFee(env, 1)));
}
void
testSellFlagExceedLimit(FeatureBitset features)
{
testcase("Offer tfSell: 2x Sell Exceed Limit");
using namespace jtx;
Env env{*this, features};
auto const startingXrp = XRP(100) + reserve(env, 1) + env.current()->fees().base * 2;
env.fund(startingXrp, gw_, alice_);
env.fund(XRP(2'000), bob_);
env.close();
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(2'200'000'000)));
AMM const ammBob(env, bob_, XRP(1'000), btc(2'200'000'000));
// Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
// Ask for more than available to prove reserve works.
// Taker pays 100'000'000 BTC for 100 XRP.
// Selling XRP.
// Will sell all 100 XRP and get more BTC than asked for.
env(offer(alice_, btc(100'000'000), XRP(200)), Json(jss::Flags, tfSell));
BEAST_EXPECT(ammBob.expectBalances(XRP(1'100), btc(2'000'000'000), ammBob.tokens()));
env.require(Balance(alice_, btc(200'000'000)));
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 startingXrp = XRP(100.1) + reserve(env, 1) + env.current()->fees().base * 2;
env.fund(startingXrp, gw_, alice_, bob_);
MPTTester const xts(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const xxx(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammAlice(env, alice_, xts(1'000'000'000'000'000), xxx(1'000'000'000'000'000));
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_, xxx(10'000'000'000'000));
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] =
xts(15'000'000'000'000).value().getJson(JsonOptions::Values::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");
BEAST_EXPECT(ammAlice.expectBalances(
xts(1'010'101'010'101'011), xxx(990'000'000'000'000), ammAlice.tokens()));
env.require(Balance(bob_, xts(989'898'989'898'989)));
env.require(Balance(bob_, xxx(1'010'000'000'000'000)));
}
void
testBridgedCross(FeatureBitset features)
{
testcase("Bridged Crossing");
using namespace jtx;
{
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
// The scenario:
// o BTC/XRP AMM is created.
// o ETH/XRP AMM is created.
// o carol has ETH but wants BTC.
// Note that carol's offer must come last. If carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100));
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol_, btc(100'000'000), eth(100'000'000)));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens()));
env.require(Balance(carol_, btc(15'100'000'000)));
env.require(Balance(carol_, eth(14'900'000'000)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
{
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
// The scenario:
// o BTC/XRP AMM is created.
// o ETH/XRP offer is created.
// o carol has ETH but wants BTC.
// 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 const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
env(offer(bob_, eth(100'000'000), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol_, btc(100'000'000), eth(100'000'000)));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
env.require(Balance(carol_, btc(15'100'000'000)));
env.require(Balance(carol_, eth(14'900'000'000)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
BEAST_EXPECT(expectOffers(env, bob_, 0));
}
{
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 15'000'000'000,
.flags = kMptDexFlags});
// The scenario:
// o BTC/XRP offer is created.
// o ETH/XRP AMM is created.
// o carol has ETH but wants BTC.
// 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), btc(100'000'000)));
env.close();
AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100));
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Carol's offer.
env(offer(carol_, btc(100'000'000), eth(100'000'000)));
env.close();
BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens()));
env.require(Balance(carol_, btc(15'100'000'000)));
env.require(Balance(carol_, eth(14'900'000'000)));
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;
{
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 20'000'000'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000));
// alice submits a tfSell | tfFillOrKill offer that does not cross.
env(offer(alice_, btc(2'100'000), XRP(210), tfSell | tfFillOrKill), Ter(tecKILLED));
BEAST_EXPECT(ammBob.expectBalances(XRP(20'000), btc(200'000'000), ammBob.tokens()));
BEAST_EXPECT(expectOffers(env, bob_, 0));
}
{
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000));
// alice submits a tfSell | tfFillOrKill offer that crosses.
// Even though tfSell is present it doesn't matter this time.
env(offer(alice_, btc(2'000'000'000'000), XRP(220), tfSell | tfFillOrKill));
env.close();
BEAST_EXPECT(
ammBob.expectBalances(XRP(20'220), btc(197'823'936'696'341), ammBob.tokens()));
env.require(Balance(alice_, btc(1'002'176'063'303'659)));
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};
env.fund(XRP(30'000), gw_, alice_, bob_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000));
env(offer(alice_, btc(10'000'000'000'000), XRP(1'500), tfSell | tfFillOrKill));
env.close();
BEAST_EXPECT(
ammBob.expectBalances(XRP(21'500), btc(186'046'511'627'907), ammBob.tokens()));
env.require(Balance(alice_, btc(1'013'953'488'372'093)));
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.01996e3BTC.
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.pay = 10'000'000'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(5000), btc(10'000'000));
env(offer(alice_, btc(1'000'000), 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/BTC. Alice places BTC/XRP offer.
{
Env env(*this, features);
env.fund(XRP(30'000), gw_, bob_, carol_);
env.fund(XRP(40'000), alice_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 30'000'000,
.flags = kMptDexFlags});
env(pay(gw_, alice_, btc(10'100'000)));
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000));
env.close();
env(offer(carol_, btc(100'000), XRP(100)));
env.close();
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens()));
env.require(Balance(carol_, btc(30'100'000)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
{
Env env(*this, features);
env.fund(XRP(30'000), gw_, bob_, carol_);
env.fund(XRP(40'100), alice_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 30'000'000,
.flags = kMptDexFlags});
env(pay(gw_, alice_, btc(10'000'000)));
AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000));
env.close();
env(offer(carol_, XRP(100), btc(100'000)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000), ammAlice.tokens()));
// Carol pays 25% transfer fee
env.require(Balance(carol_, btc(29'875'000)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
}
{
// Bridged crossing.
Env env{*this, features};
env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 15'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 15'000'000,
.flags = kMptDexFlags});
// The scenario:
// o BTC/XRP AMM is created.
// o ETH/XRP Offer is created.
// o carol has ETH but wants BTC.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000));
env(offer(bob_, eth(100'000), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// fully consumes Bob's offer.
env(offer(carol_, btc(100'000), eth(100'000)));
env.close();
// AMM doesn't pay the transfer fee
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens()));
env.require(Balance(carol_, btc(15'100'000)));
// Carol pays 25% transfer fee.
env.require(Balance(carol_, eth(14'875'000)));
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};
env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 15'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 15'000'000,
.flags = kMptDexFlags});
// The scenario:
// o BTC/XRP AMM is created.
// o ETH/XRP Offer is created.
// o carol has ETH but wants BTC.
// Note that Carol's offer must come last. If Carol's offer is
// placed before AMM is created, then autobridging will not occur.
AMM const ammAlice(env, alice_, XRP(10'000), btc(10'050'000));
env(offer(bob_, eth(100'000), XRP(100)));
env.close();
// Carol makes an offer that consumes AMM liquidity and
// partially consumes Bob's offer.
env(offer(carol_, btc(50'000), eth(50'000)));
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), btc(10'000'000), ammAlice.tokens()));
env.require(Balance(carol_, btc(15'050'000)));
// Carol pays 25% transfer fee.
env.require(Balance(carol_, eth(14'937'500)));
BEAST_EXPECT(expectOffers(env, carol_, 0));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{eth(50'000), XRP(50)}}}));
}
}
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 f = env.current()->fees().base;
env.fund(XRP(30'000) + f, alice_, bob_);
env.close();
MPTTester const btc(
{.env = env, .issuer = bob_, .holders = {alice_}, .flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(10'000), btc(10'100));
env(offer(alice_, btc(100), XRP(100)));
env.close();
BEAST_EXPECT(ammBob.expectBalances(XRP(10'100), btc(10'000), ammBob.tokens()));
BEAST_EXPECT(expectOffers(env, alice_, 0));
env.require(Balance(alice_, btc(100)));
}
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 fee = env.current()->fees().base;
env.fund(XRP(1'000), carol);
env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam);
env.close();
MPTTester const aBux(
{.env = env, .issuer = ann, .holders = {bob, cam, carol}, .flags = kMptDexFlags});
MPTTester const bBux(
{.env = env, .issuer = bob, .holders = {ann, cam, carol}, .flags = kMptDexFlags});
env(pay(ann, cam, aBux(350'000'000'000'000)));
env(pay(bob, cam, bBux(350'000'000'000'000)));
env(pay(bob, carol, bBux(4'000'000'000'000'000)));
env(pay(ann, carol, aBux(4'000'000'000'000'000)));
AMM const ammCarol(env, carol, aBux(3'000'000'000'000'000), bBux(3'300'000'000'000'000));
// 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, aBux(290'000'000'000'000), bBux(300'000'000'000'000), tfPassive));
env.close();
env.require(Balance(cam, aBux(350'000'000'000'000)));
env.require(Balance(cam, bBux(350'000'000'000'000)));
env.require(offers(cam, 1));
// This offer caused the assert.
env(offer(cam, bBux(300'000'000'000'000), aBux(300'000'000'000'000)));
// AMM is consumed up to the first cam Offer quality
BEAST_EXPECT(ammCarol.expectBalances(
aBux(3'093'541'659'651'604), bBux(3'200'215'509'984'418), ammCarol.tokens()));
BEAST_EXPECT(expectOffers(
env, cam, 1, {{Amounts{bBux(200'215'509'984'418), aBux(200'215'509'984'419)}}}));
}
void
testRequireAuth(FeatureBitset features)
{
testcase("RequireAuth");
using namespace jtx;
Env env{*this, features};
env.fund(XRP(400'000), gw_, alice_, bob_);
MPTTester btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.flags = tfMPTRequireAuth | kMptDexFlags});
// Authorize bob and alice
btc.authorize({.holder = alice_});
btc.authorize({.holder = bob_});
env(pay(gw_, alice_, btc(1'000)));
env.close();
// Alice is able to create AMM since the GW has authorized her
AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050));
env(pay(gw_, bob_, btc(50)));
env.close();
env.require(Balance(bob_, btc(50)));
// Bob's offer should cross Alice's AMM
env(offer(bob_, XRP(50), btc(50)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, bob_, 0));
env.require(Balance(bob_, btc(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();
MPTTester btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.flags = tfMPTRequireAuth | kMptDexFlags});
// Alice doesn't have the funds
{
AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH));
}
btc.authorize({.holder = bob_});
env(pay(gw_, bob_, btc(50)));
env.close();
env.require(Balance(bob_, btc(50)));
// Alice should not be able to create AMM without authorization.
{
AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH));
}
// Finally, authorize alice. Now alice's AMM create should succeed.
btc.authorize({.holder = alice_});
env(pay(gw_, alice_, btc(1'000)));
env.close();
AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050));
// Authorize AMM.
// BTC.authorize({.account = ammAlice.ammAccount()});
// env.close();
// Now bob creates his offer again, which crosses with alice's AMM.
env(offer(bob_, XRP(50), btc(50)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, bob_, 0));
env.require(Balance(bob_, btc(0)));
}
void
testOffers()
{
using namespace jtx;
FeatureBitset const all{testableAmendments()};
testRmFundedOffer(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);
testBridgedCross(all);
testSellWithFillOrKill(all);
testTransferRateOffer(all);
testSelfIssueOffer(all);
testSellFlagBasic(all);
testDirectToDirectPath(all);
testRequireAuth(all);
testMissingAuth(all);
}
void
pathFindConsumeAll()
{
testcase("path find consume all");
using namespace jtx;
Env env = pathTestEnv();
env.fund(XRP(100'000'260), alice_);
env.fund(XRP(30'000), gw_, bob_, carol_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 100'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammCarol(env, carol_, XRP(100), eth(100'000'000'000'000));
STPathSet st;
STAmount sa;
STAmount da;
std::tie(st, sa, da) = findPaths(
env, alice_, bob_, bob_["AUD"](-1), std::optional<STAmount>(XRP(100'000'000)));
BEAST_EXPECT(st.empty());
std::tie(st, sa, da) =
findPaths(env, alice_, bob_, eth(-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.99e12ETH. This is the amount Bob
// can get out of AMM for 100,000,000XRP.
BEAST_EXPECT(equal(da, eth(99'999'900'000'100)));
}
// carol holds ETH, sells ETH for XRP
// bob will hold ETH
// alice pays bob ETH using XRP
void
viaOffersViaGateway()
{
testcase("via gateway");
using namespace jtx;
Env env = pathTestEnv();
env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
env.close();
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 10'000,
.flags = kMptDexFlags});
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 10'000,
.flags = kMptDexFlags});
env(pay(gw_, carol_, eth(51)));
env.close();
AMM const ammCarol(env, carol_, XRP(40), eth(51));
env(pay(alice_, bob_, eth(10)), Sendmax(XRP(100)), Paths(XRP));
env.close();
// AMM offer is 51.282052XRP/11ETH, 11ETH/1.1 = 10ETH to bob
BEAST_EXPECT(ammCarol.expectBalances(XRP(51), eth(40), ammCarol.tokens()));
env.require(Balance(bob_, eth(10)));
auto const result = findPaths(env, alice_, bob_, btc(25));
BEAST_EXPECT(std::get<0>(result).empty());
}
void
receiveMax()
{
testcase("Receive max");
using namespace jtx;
auto const charlie = Account("charlie");
{
// XRP -> MPT receive max
Env env = pathTestEnv();
env.fund(XRP(30'000), alice_, bob_, charlie, gw_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, charlie},
.pay = 11'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammCharlie(env, charlie, XRP(10), eth(11'000'000'000'000));
auto [st, sa, da] = findPaths(env, alice_, bob_, eth(-1), XRP(1).value());
BEAST_EXPECT(sa == XRP(1));
BEAST_EXPECT(equal(da, eth(1'000'000'000'000)));
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.getMPTID() == eth.issuanceID());
}
}
{
// MPT -> XRP receive max
Env env = pathTestEnv();
env.fund(XRP(30'000), alice_, bob_, charlie, gw_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, charlie},
.pay = 11'000'000'000'000,
.flags = kMptDexFlags});
AMM const ammCharlie(env, charlie, XRP(11), eth(10'000'000'000'000));
env.close();
auto [st, sa, da] =
findPaths(env, alice_, bob_, drops(-1), eth(1'000'000'000'000).value());
BEAST_EXPECT(sa == eth(1'000'000'000'000));
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
pathFind01()
{
testcase("Path Find: XRP -> XRP and XRP -> MPT");
using namespace jtx;
Env env = pathTestEnv();
Account a1{"A1"};
Account a2{"A2"};
Account a3{"A3"};
Account const g1{"G1"};
Account const 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();
MPTTester const xyzG1(
{.env = env, .issuer = g1, .holders = {a1, m1, a2}, .flags = kMptDexFlags});
MPTTester const xyzG2(
{.env = env, .issuer = g2, .holders = {a2, m1, a1}, .flags = kMptDexFlags});
MPTTester const abcG3(
{.env = env, .issuer = g3, .holders = {a1, a2, m1, a3}, .flags = kMptDexFlags});
MPTTester const abcA2(
{.env = env, .issuer = a2, .holders = {g3, a1}, .flags = kMptDexFlags});
env(pay(g1, a1, xyzG1(3'500'000'000)));
env(pay(g3, a1, abcG3(1'200'000'000)));
env(pay(g1, m1, xyzG1(25'000'000'000)));
env(pay(g2, m1, xyzG2(25'000'000'000)));
env(pay(g3, m1, abcG3(25'000'000'000)));
env(pay(a2, g3, abcA2(101'000'000)));
env.close();
AMM const ammM1XyzG1XyzG2(env, m1, xyzG1(1'000'000'000), xyzG2(1'000'000'000));
AMM const ammM1XrpAbcG3(env, m1, XRP(10'000), abcG3(1'000'000'000));
AMM const ammG3AbcG3AbcA2(env, g3, abcG3(100'000'000), abcA2(101'000'000));
env.close();
STPathSet st;
STAmount sa, da;
{
auto const& sendAmt = XRP(10);
std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(st.empty());
}
{
// no path should exist for this since dest account
// does not exist.
auto const& sendAmt = XRP(200);
std::tie(st, sa, da) =
findPaths(env, a1, Account{"A0"}, sendAmt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(st.empty());
}
{
auto const& sendAmt = abcG3(10'000'000);
std::tie(st, sa, da) = findPaths(env, a2, g3, sendAmt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(equal(sa, XRPAmount{101'010'102}));
BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3)))));
}
{
auto const& sendAmt = abcA2(1'000'000);
std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency());
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(equal(sa, XRPAmount{10'010'011}));
BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3)), ipe(MPT(abcA2)))));
}
}
void
pathFind02()
{
testcase("Path Find: non-XRP -> XRP");
using namespace jtx;
Env env = pathTestEnv();
Account a1{"A1"};
Account a2{"A2"};
Account const g3{"G3"};
Account m1{"M1"};
env.fund(XRP(1'000), a1, a2, g3);
env.fund(XRP(11'000), m1);
env.close();
MPTTester const eth(
{.env = env,
.issuer = g3,
.holders = {a1, a2, m1},
.pay = 1'000'000'000,
.flags = kMptDexFlags});
AMM const ammM1(env, m1, eth(1'000'000'000), XRP(10'010));
STPathSet st;
STAmount sa, da;
auto const& sendAmt = XRP(10);
std::tie(st, sa, da) =
findPathsByElement(env, a1, a2, sendAmt, std::nullopt, ipe(MPT(eth)));
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(equal(sa, eth(1'000'000)));
BEAST_EXPECT(same(st, stpath(ipe(xrpIssue()))));
}
void
pathFind06()
{
testcase("Path Find: non-XRP -> non-XRP, same issuanceID");
using namespace jtx;
{
Env env = pathTestEnv();
Account a1{"A1"};
Account a2{"A2"};
Account const a3{"A3"};
Account const g1{"G1"};
Account const g2{"G2"};
Account m1{"M1"};
env.fund(XRP(11'000), m1);
env.fund(XRP(1'000), a1, a2, a3, g1, g2);
env.close();
MPTTester const hkdG1(
{.env = env,
.issuer = g1,
.holders = {a1, m1},
.pay = 5'000'000'000,
.flags = kMptDexFlags});
MPTTester const hkdG2(
{.env = env,
.issuer = g2,
.holders = {a2, m1},
.pay = 5'000'000'000,
.flags = kMptDexFlags});
AMM const ammM1(env, m1, hkdG1(1'000'000'000), hkdG2(1'010'000'000));
auto const& sendAmt = hkdG2(10'000'000);
STPathSet st;
STAmount sa, da;
std::tie(st, sa, da) = jtx::findPaths(
env, g1, a2, sendAmt, std::nullopt, hkdG1.issuanceID(), std::nullopt, std::nullopt);
BEAST_EXPECT(equal(da, sendAmt));
BEAST_EXPECT(equal(sa, hkdG1(10'000'000)));
BEAST_EXPECT(same(st, stpath(ipe(MPT(hkdG2)))));
}
}
void
testFalseDry(FeatureBitset features)
{
testcase("falseDryChanges");
using namespace jtx;
Env env(*this, features);
env.memoize(bob_);
env.fund(XRP(10'000), alice_, gw_);
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();
MPTTester const eth(
{.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
env(pay(gw_, alice_, eth(50'000)));
env(pay(gw_, bob_, btc(150'000)));
// 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/BTC offer will require two
// recursive calls to the ETH/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_, eth(50'000), XRP(50)));
AMM const ammBob(env, bob_, ammxrpPool, btc(150'000));
env(pay(alice_, carol_, btc(1'000'000'000)),
Path(~XRP, ~MPT(btc)),
Sendmax(eth(500'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
auto const carolBTC = env.balance(carol_, MPT(btc));
BEAST_EXPECT(carolBTC > btc(0) && carolBTC < btc(50'000));
}
void
testBookStep(FeatureBitset features)
{
testcase("Book Step");
using namespace jtx;
// simple MPT/IOU mix offer
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 100'000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 100'000'000});
env(pay(gw_, alice_, btc(500000)));
env(pay(gw_, bob_, btc(500000)));
env(pay(gw_, carol_, btc(500000)));
env(pay(gw_, alice_, eth(500000)));
env(pay(gw_, bob_, eth(500000)));
env(pay(gw_, carol_, eth(500000)));
env.close();
AMM const ammBob(env, bob_, btc(100'000), eth(150'000));
env(pay(alice_, carol_, eth(50'000)), Path(~eth), Sendmax(btc(50'000)));
env.require(Balance(alice_, btc(450'000)));
env.require(Balance(bob_, btc(400'000)));
env.require(Balance(bob_, eth(350'000)));
env.require(Balance(carol_, eth(550'000)));
BEAST_EXPECT(ammBob.expectBalances(btc(150'000), eth(100'000), ammBob.tokens()));
};
testHelper2TokensMix(test);
}
{
// simple MPT/XRP XRP/MPT offer
Env env(*this, features);
env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 100'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 150'000,
.flags = kMptDexFlags});
AMM const ammBobBtcXrp(env, bob_, btc(100'000), XRP(150));
AMM const ammBobXrpEth(env, bob_, XRP(100), eth(150'000));
env(pay(alice_, carol_, eth(50'000)), Path(~XRP, ~MPT(eth)), Sendmax(btc(50'000)));
env.require(Balance(alice_, btc(50'000)));
env.require(Balance(bob_, btc(0)));
env.require(Balance(bob_, eth(0)));
env.require(Balance(carol_, eth(200'000)));
BEAST_EXPECT(
ammBobBtcXrp.expectBalances(btc(150'000), XRP(100), ammBobBtcXrp.tokens()));
BEAST_EXPECT(
ammBobXrpEth.expectBalances(XRP(150), eth(100'000), ammBobXrpEth.tokens()));
}
{
// simple XRP -> MPT through offer and sendmax
Env env(*this, features);
XRPAmount const baseFee{env.current()->fees().base};
env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 150'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(100), eth(150'000));
env(pay(alice_, carol_, eth(50'000)), Path(~MPT(eth)), Sendmax(XRP(50)));
BEAST_EXPECT(expectLedgerEntryRoot(env, alice_, XRP(10'000) - XRP(50) - 2 * baseFee));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob_, XRP(10'000) - XRP(100) - ammCrtFee(env) - baseFee));
env.require(Balance(bob_, eth(0)));
env.require(Balance(carol_, eth(200'000)));
BEAST_EXPECT(ammBob.expectBalances(XRP(150), eth(100'000), ammBob.tokens()));
}
{
// simple MPT -> XRP through offer and sendmax
Env env(*this, features);
XRPAmount const baseFee{env.current()->fees().base};
env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 100'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, eth(100'000), XRP(150));
env(pay(alice_, carol_, XRP(50)), Path(~XRP), Sendmax(eth(50'000)));
env.require(Balance(alice_, eth(50'000)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob_, XRP(10'000) - XRP(150) - ammCrtFee(env) - baseFee));
env.require(Balance(bob_, eth(0)));
BEAST_EXPECT(expectLedgerEntryRoot(env, carol_, XRP(10'000 + 50) - baseFee));
BEAST_EXPECT(ammBob.expectBalances(eth(150'000), XRP(100), ammBob.tokens()));
}
// test unfunded offers are removed when payment succeeds
{
auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
Env env(*this, features);
env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
env.close();
auto const btc = issue1(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1000'000000});
auto const eth = issue2(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1000'000000});
auto const gbp = issue3(
{.env = env,
.token = "GBP",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1000'000000});
env(pay(gw_, alice_, btc(60'000)));
env(pay(gw_, bob_, eth(200'000)));
env(pay(gw_, bob_, gbp(150'000)));
env(offer(bob_, btc(50'000), eth(50'000)));
env(offer(bob_, btc(40'000), gbp(50'000)));
env.close();
AMM const ammBob(env, bob_, gbp(100'000), eth(150'000));
// unfund offer
env(pay(bob_, gw_, gbp(50'000)));
BEAST_EXPECT(isOffer(env, bob_, btc(50'000), eth(50'000)));
BEAST_EXPECT(isOffer(env, bob_, btc(40'000), gbp(50'000)));
env(pay(alice_, carol_, eth(50'000)),
Path(~eth),
Path(~gbp, ~eth),
Sendmax(btc(60'000)));
env.require(Balance(alice_, btc(10'000)));
env.require(Balance(bob_, btc(50'000)));
env.require(Balance(bob_, eth(0)));
env.require(Balance(bob_, gbp(0)));
env.require(Balance(carol_, eth(50'000)));
// used in the payment
BEAST_EXPECT(!isOffer(env, bob_, btc(50'000), eth(50'000)));
// found unfunded
BEAST_EXPECT(!isOffer(env, bob_, btc(40'000), gbp(50'000)));
// unchanged
BEAST_EXPECT(ammBob.expectBalances(gbp(100'000), eth(150'000), ammBob.tokens()));
};
testHelper3TokensMix(test);
}
{
// test unfunded offers are removed when the payment fails.
// bob makes two offers: a funded 50'000'000 ETH for 50'000'000 BTC
// and an unfunded 50'000'000 GBP for 60'000'000 BTC. alice pays
// carol 61'000'000 ETH with 61'000'000 BTC. alice only has
// 60'000'000 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);
MPTTester btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.flags = kMptDexFlags});
MPTTester eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.flags = kMptDexFlags});
MPTTester gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.flags = kMptDexFlags});
env(pay(gw_, alice_, btc(60'000'000)));
env(pay(gw_, bob_, btc(100'000'000)));
env(pay(gw_, bob_, eth(100'000'000)));
env(pay(gw_, bob_, gbp(50'000'000)));
env(pay(gw_, carol_, gbp(1'000'000)));
env.close();
// This is multiplath, which generates limited # of offers
AMM const ammBobBtcEth(env, bob_, btc(50'000'000), eth(50'000'000));
env(offer(bob_, btc(60'000'000), gbp(50'000'000)));
env(offer(carol_, btc(1'000'000'000), gbp(1'000'000)));
env(offer(bob_, gbp(50'000'000), eth(50'000'000)));
// unfund offer
env(pay(bob_, gw_, gbp(50'000'000)));
BEAST_EXPECT(ammBobBtcEth.expectBalances(
btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens()));
BEAST_EXPECT(isOffer(env, bob_, btc(60'000'000), gbp(50'000'000)));
BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000)));
BEAST_EXPECT(isOffer(env, bob_, gbp(50'000'000), eth(50'000'000)));
auto flowJournal = env.app().getLogs().journal("Flow");
auto const flowResult = [&] {
STAmount const deliver(eth(51'000'000));
STAmount smax(btc(61'000'000));
PaymentSandbox sb(env.current().get(), TapNone);
STPathSet paths;
auto ipe = [](MPTTester const& iss) {
return STPathElement(
STPathElement::TypeMpt | STPathElement::TypeIssuer,
xrpAccount(),
PathAsset{iss.issuanceID()},
iss.issuer());
};
{
// BTC -> ETH
STPath const p1({ipe(eth)});
paths.pushBack(p1);
// BTC -> GBP -> ETH
STPath const p2({ipe(gbp), ipe(eth)});
paths.pushBack(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().getOpenLedger().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(ammBobBtcEth.expectBalances(
btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens()));
BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000)));
// found unfunded
BEAST_EXPECT(!isOffer(env, bob_, btc(60'000'000), gbp(50'000'000)));
}
{
// Do not produce more in the forward pass than the reverse pass
// This test uses a path that whose reverse pass will compute a
// 500 ETH input required for a 1'000 BTC output. It sets a sendmax
// of 400 ETH, so the payment engine will need to do a forward
// pass. Without limits, the 400 ETH would produce 1'000 BTC in
// the forward pass. This test checks that the payment produces
// 1'000 BTC, as expected.
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 10'000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 10'000'000});
env(pay(gw_, alice_, eth(1'000'000)));
env(pay(gw_, bob_, btc(1'000'000)));
env(pay(gw_, bob_, eth(1'000'000)));
env.close();
AMM const ammBob(env, bob_, eth(8'000), XRPAmount{21});
env(offer(bob_, drops(1), btc(1'000'000)), Txflags(tfPassive));
env(pay(alice_, carol_, btc(1'000)),
Path(~XRP, ~btc),
Sendmax(eth(400)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.require(Balance(carol_, btc(1'000)));
BEAST_EXPECT(ammBob.expectBalances(eth(8400), XRPAmount{20}, ammBob.tokens()));
};
testHelper2TokensMix(test);
}
}
void
testTransferRateNoOwnerFee(FeatureBitset features)
{
testcase("No Owner Fee");
using namespace jtx;
{
// payment via AMM
Env env(*this, features);
env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000));
env(pay(alice_, carol_, btc(100'000'000'000'000)),
Path(~MPT(btc)),
Sendmax(gbp(150'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
// alice buys 107.1428e12BTC with 120e12GBP and pays 25% tr fee on
// 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP
env.require(Balance(alice_, gbp(850'000'000'000'000)));
BEAST_EXPECT(amm.expectBalances(
gbp(1'120'000'000'000'000), btc(892'857'142'857'143), amm.tokens()));
// 25% of 85.7142e12BTC is paid in tr fee
// 85.7142e12*1.25 = 107.1428e12BTC
env.require(Balance(carol_, btc(1'085'714'285'714'285)));
}
{
// Payment via offer and AMM
Env env(*this, features);
Account const ed("ed");
env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)),
Txflags(tfPassive));
env.close();
AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000));
env(pay(alice_, carol_, btc(100'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(150'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
// alice buys 120e12ETH with 120e12GBP via the offer
// and pays 25% tr fee on 120e12GBP
// 1,000e12 - 120e12*1.25 = 850e12GBP
env.require(Balance(alice_, gbp(850'000'000'000'000)));
// consumed offer is 120e12GBP/120e12ETH
// ed doesn't pay tr fee
env.require(Balance(ed, eth(880'000'000'000'000)));
env.require(Balance(ed, gbp(1'120'000'000'000'000)));
BEAST_EXPECT(expectOffers(
env, ed, 1, {Amounts{gbp(880'000'000'000'000), eth(880'000'000'000'000)}}));
// 25% on 96e12ETH is paid in tr fee 96e12*1.25 = 120e12ETH
// 96e12ETH is swapped in for 87.5912e12BTC
BEAST_EXPECT(amm.expectBalances(
eth(1'096'000'000'000'000), btc(912'408'759'124'088), amm.tokens()));
// 25% on 70.0729e12BTC is paid in tr fee 70.0729e12*1.25
// = 87.5912e12BTC
env.require(Balance(carol_, btc(1'070'072'992'700'729)));
}
{
// Payment via AMM, AMM
Env env(*this, features);
Account const ed("ed");
env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000));
env(pay(alice_, carol_, btc(100'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(150'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment));
env.close();
env.require(Balance(alice_, gbp(850'000'000'000'000)));
// alice buys 107.1428e12ETH with 120e12GBP and pays 25% tr fee on
// 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP 120e12GBP is swapped
// in for 107.1428e12ETH
BEAST_EXPECT(amm1.expectBalances(
gbp(1'120'000'000'000'000), eth(892'857'142'857'143), amm1.tokens()));
// 25% on 85.7142e12ETH is paid in tr fee 85.7142e12*1.25 =
// 107.1428e12ETH 85.7142e12ETH is swapped in for 78.9473e12BTC
BEAST_EXPECT(amm2.expectBalances(
eth(1'085'714'285'714'285), btc(921'052'631'578'948), amm2.tokens()));
// 25% on 63.1578e12BTC is paid in tr fee 63.1578e12*1.25
// = 78.9473e12BTC
env.require(Balance(carol_, btc(1'063'157'894'736'841)));
}
{
// AMM offer crossing
Env env(*this, features);
env.fund(XRP(1'000), gw_, alice_, bob_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.transferFee = 25'000,
.pay = 1'100'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_},
.transferFee = 25'000,
.pay = 1'100'000,
.flags = kMptDexFlags});
AMM const amm(env, bob_, btc(1'000'000), eth(1'100'000));
env(offer(alice_, eth(100'000), btc(100'000)));
env.close();
// 100e3BTC is swapped in for 100e3ETH
BEAST_EXPECT(amm.expectBalances(btc(1'100'000), eth(1'000'000), amm.tokens()));
// alice pays 25% tr fee on 100e3BTC 1100e3-100e3*1.25 = 975e3BTC
env.require(Balance(alice_, btc(975'000)));
env.require(Balance(alice_, eth(1'200'000)));
BEAST_EXPECT(expectOffers(env, alice_, 0));
}
{
// Payment via AMM with limit quality
Env env(*this, features);
env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'000'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000));
// requested quality limit is 100e12BTC/178.58e12GBP = 0.55997
// trade quality is 100e12BTC/178.5714 = 0.55999e12
env(pay(alice_, carol_, btc(100'000'000'000'000)),
Path(~MPT(btc)),
Sendmax(gbp(178'580'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 125e12BTC with 142.8571e12GBP and pays 25% tr fee
// on 142.8571e12GBP
// 1,000e12 - 142.8571e12*1.25 = 821.4285e12GBP
env.require(Balance(alice_, gbp(821'428'571'428'571)));
// 142.8571e12GBP is swapped in for 125e12BTC
BEAST_EXPECT(amm.expectBalances(
gbp(1'142'857'142'857'143), btc(875'000'000'000'000), amm.tokens()));
// 25% on 100e12BTC is paid in tr fee
// 100e12*1.25 = 125e12BTC
env.require(Balance(carol_, btc(1'100'000'000'000'000)));
}
{
// Payment via AMM with limit quality, deliver less
// than requested
Env env(*this, features);
env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'200'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'200'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'200'000'000'000'000));
// requested quality limit is 90e12BTC/120e12GBP = 0.75
// trade quality is 22.5e12BTC/30e12GBP = 0.75
env(pay(alice_, carol_, btc(90'000'000'000'000)),
Path(~MPT(btc)),
Sendmax(gbp(120'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 28.125e12BTC with 24e12GBP and pays 25% tr fee
// on 24e12GBP
// 1,200e12 - 24e12*1.25 =~ 1,170e12GBP
env.require(Balance(alice_, gbp(1'170'000'000'000'000)));
// 24e12GBP is swapped in for 28.125e12BTC
BEAST_EXPECT(amm.expectBalances(
gbp(1'024'000'000'000'000), btc(1'171'875'000'000'000), amm.tokens()));
// 25% on 22.5e12BTC is paid in tr fee
// 22.5*1.25 = 28.125e12BTC
env.require(Balance(carol_, btc(1'222'500'000'000'000)));
}
{
// Payment via offer and AMM with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)),
Txflags(tfPassive));
env.close();
AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
// requested quality limit is 95e12BTC/140e12GBP = 0.6785
// trade quality is 59.7321e12BTC/88.0262e12GBP = 0.6785
env(pay(alice_, carol_, btc(95'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(140'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 70.4210e12ETH with 70.4210e12GBP via the offer
// and pays 25% tr fee on 70.4210e12GBP
// 1,400e12 - 70.4210e12*1.25 = 1400e12 - 88.0262e12 =
// 1311.9736e12GBP
env.require(Balance(alice_, gbp(1'311'973'684'210'525)));
// ed doesn't pay tr fee, the balances reflect consumed offer
// 70.4210e12GBP/70.4210e12ETH
env.require(Balance(ed, eth(1'329'578'947'368'420)));
env.require(Balance(ed, gbp(1'470'421'052'631'580)));
BEAST_EXPECT(expectOffers(
env, ed, 1, {Amounts{gbp(929'578'947'368'420), eth(929'578'947'368'420)}}));
// 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25
// = 70.4210e12ETH 56.3368e12ETH is swapped in for 74.6651e12BTC
BEAST_EXPECT(amm.expectBalances(
eth(1'056'336'842'105'264), btc(1'325'334'821'428'571), amm.tokens()));
// 25% on 59.7321e12BTC is paid in tr fee 59.7321e12*1.25
// = 74.6651e12BTC
env.require(Balance(carol_, btc(1'459'732'142'857'143)));
}
{
// Payment via AMM and offer with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
env(offer(ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)),
Txflags(tfPassive));
env.close();
// requested quality limit is 95e12BTC/140e12GBP = 0.6785
// trade quality is 47.7857e12BTC/70.4210e12GBP = 0.6785
env(pay(alice_, carol_, btc(95'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(140'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 53.3322e12ETH with 56.3368e12GBP via the amm
// and pays 25% tr fee on 56.3368e12GBP
// 1,400e12 - 56.3368e12*1.25 = 1400e12 - 70.4210e12 =
// 1329.5789e12GBP
env.require(Balance(alice_, gbp(1'329'578'947'368'420)));
//// 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25
///= 70.4210e12ETH
// 56.3368e12GBP is swapped in for 53.3322e12ETH
BEAST_EXPECT(amm.expectBalances(
gbp(1'056'336'842'105'264), eth(946'667'729'591'836), amm.tokens()));
// 25% on 42.6658e12ETH is paid in tr fee 42.6658e12*1.25
// = 53.3322e12ETH 42.6658e12ETH/59.7321e12BTC
env.require(Balance(ed, btc(1'340'267'857'142'857)));
env.require(Balance(ed, eth(1'442'665'816'326'531)));
BEAST_EXPECT(expectOffers(
env, ed, 1, {Amounts{eth(957'334'183'673'469), btc(1'340'267'857'142'857)}}));
// 25% on 47.7857e12BTC is paid in tr fee 47.7857e12*1.25
// = 59.7321e12BTC
env.require(Balance(carol_, btc(1'447'785714285714)));
}
{
// Payment via AMM, AMM with limit quality, deliver less
// than requested
Env env(*this, features);
Account const ed("ed");
env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, ed},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
// requested quality limit is 90e12BTC/145e12GBP = 0.6206
// trade quality is 66.7432e12BTC/107.5308e12GBP = 0.6206
env(pay(alice_, carol_, btc(90'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(145'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// alice buys 53.3322e12ETH with 107.5308e12GBP
// 25% on 86.0246e12GBP is paid in tr fee
// 1,400e12 - 86.0246e12*1.25 = 1400e12 - 107.5308e12 =
// 1229.4691e12GBP
env.require(Balance(alice_, gbp(1'292'469'135'802'465)));
// 86.0246e12GBP is swapped in for 79.2106e12ETH
BEAST_EXPECT(amm1.expectBalances(
gbp(1'086'024'691'358'028), eth(920'789'377'955'618), amm1.tokens()));
// 25% on 63.3684e12ETH is paid in tr fee 63.3684e12*1.25
// = 79.2106e12ETH 63.3684e12ETH is swapped in for 83.4291e12BTC
BEAST_EXPECT(amm2.expectBalances(
eth(1'063'368'497'635'505), btc(1'316'570'881'226'053), amm2.tokens()));
// 25% on 66.7432e12BTC is paid in tr fee 66.7432e12*1.25
// = 83.4291e12BTC
env.require(Balance(carol_, btc(1'466'743'295'019'157)));
}
{
// Payment by the issuer via AMM, AMM with limit quality,
// deliver less than requested
Env env(*this, features);
env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const gbp(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.transferFee = 25'000,
.pay = 1'400'000'000'000'000,
.flags = kMptDexFlags});
AMM const amm1(env, alice_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
AMM const amm2(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
// requested quality limit is 90e12BTC/120e12GBP = 0.75
// trade quality is 81.1111e12BTC/108.1481e12GBP = 0.75
env(pay(gw_, carol_, btc(90'000'000'000'000)),
Path(~MPT(eth), ~MPT(btc)),
Sendmax(gbp(120'000'000'000'000)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
// 108.1481e12GBP is swapped in for 97.5935e12ETH
BEAST_EXPECT(amm1.expectBalances(
gbp(1'108'148'148'148'150), eth(902'406'417'112'298), amm1.tokens()));
// 25% on 78.0748e12ETH is paid in tr fee 78.0748e12*1.25
// = 97.5935e12ETH 78.0748e12ETH is swapped in for 101.3888e12BTC
BEAST_EXPECT(amm2.expectBalances(
eth(1'078'074'866'310'161), btc(1'298'611'111'111'111), amm2.tokens()));
// 25% on 81.1111e12BTC is paid in tr fee 81.1111e12*1.25 =
// 101.3888e12BTC
env.require(Balance(carol_, btc(1'481'111'111'111'111)));
}
}
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);
env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
MPTTester const eth(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 2'000'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(1'000), eth(1'050'000));
env(offer(bob_, XRP(100), eth(50'000)));
env(pay(alice_, carol_, eth(100'000)),
Path(~MPT(eth)),
Sendmax(XRP(100)),
Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
BEAST_EXPECT(ammBob.expectBalances(XRP(1'050), eth(1'000'000), ammBob.tokens()));
env.require(Balance(carol_, eth(2'050'000)));
BEAST_EXPECT(expectOffers(env, bob_, 1, {{{XRP(100), eth(50'000)}}}));
}
}
void
testXRPPathLoop()
{
testcase("Circular XRP");
using namespace jtx;
// Payment path starting with XRP
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, gw_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
env(pay(gw_, alice_, btc(200'000)));
env(pay(gw_, bob_, btc(200'000)));
env(pay(gw_, alice_, eth(200'000)));
env(pay(gw_, bob_, eth(200'000)));
env.close();
AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(101'000));
AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(101'000));
env(pay(alice_, bob_, eth(1'000)),
Path(~btc, ~XRP, ~eth),
Sendmax(XRP(1)),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
};
testHelper2TokensMix(test);
}
// Payment path ending with XRP
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, gw_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
env(pay(gw_, alice_, btc(200'000)));
env(pay(gw_, bob_, btc(200'000)));
env(pay(gw_, alice_, eth(200'000)));
env(pay(gw_, bob_, eth(200'000)));
env.close();
AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000));
AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000));
// ETH -> //XRP -> //BTC ->XRP
env(pay(alice_, bob_, XRP(1)),
Path(~XRP, ~btc, ~XRP),
Sendmax(eth(1'000)),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
};
testHelper2TokensMix(test);
}
// Payment where loop is formed in the middle of the path, not
// on an endpoint
{
auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
Env env(*this);
env.fund(XRP(10'000), gw_, alice_, bob_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
auto const jpy = issue2(
{.env = env,
.token = "JPY",
.issuer = gw_,
.holders = {alice_, bob_},
.limit = 2000'000});
env(pay(gw_, alice_, btc(200'000)));
env(pay(gw_, bob_, btc(200'000)));
env(pay(gw_, alice_, eth(200'000)));
env(pay(gw_, bob_, eth(200'000)));
env(pay(gw_, alice_, jpy(200'000)));
env(pay(gw_, bob_, jpy(200'000)));
env.close();
AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000));
AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000));
AMM const ammAliceXrpJpy(env, alice_, XRP(100), jpy(100'000));
env(pay(alice_, bob_, jpy(1'000)),
Path(~XRP, ~eth, ~XRP, ~jpy),
Sendmax(btc(1'000)),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
};
testHelper3TokensMix(test);
}
}
void
testStepLimit(FeatureBitset features)
{
testcase("Step Limit");
using namespace jtx;
{
Env env(*this, features);
auto const dan = Account("dan");
auto const ed = Account("ed");
env.fund(XRP(100'000'000), gw_, alice_, bob_, carol_, dan, ed);
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
env(pay(gw_, ed, btc(11'000'000'000'000)));
env(pay(gw_, bob_, btc(1'000'000'000'000)));
env(pay(gw_, dan, btc(1'000'000'000'000)));
nOffers(env, 2'000, bob_, XRP(1), btc(1'000'000'000'000));
nOffers(env, 1, dan, XRP(1), btc(1'000'000'000'000));
AMM const ammEd(env, ed, XRP(9), btc(11'000'000'000'000));
// Alice offers to buy 1000 XRP for 1000e12 BTC. She takes Bob's
// first offer, removes 999 more as unfunded, then hits the step
// limit.
env(offer(alice_, btc(1'000'000'000'000'000), XRP(1'000)));
env.require(Balance(alice_, btc(2'050'125'257'867)));
env.require(Owners(alice_, 2));
env.require(Balance(bob_, btc(0)));
env.require(Owners(bob_, 1'001));
env.require(Balance(dan, btc(1'000'000'000'000)));
env.require(Owners(dan, 2));
// Carol offers to buy 1000 XRP for 1000e12 BTC. She removes Bob's
// next 1000 offers as unfunded and hits the step limit.
env(offer(carol_, btc(1'000'000'000'000'000), XRP(1'000)));
env.require(Balance(carol_, MPT(btc)(kNone)));
env.require(Owners(carol_, 1));
env.require(Balance(bob_, btc(0)));
env.require(Owners(bob_, 1));
env.require(Balance(dan, btc(1'000'000'000'000)));
env.require(Owners(dan, 2));
}
// MPT/IOU, similar to the case above
{
Env env(*this, features);
auto const dan = Account("dan");
auto const ed = Account("ed");
env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
env.close();
MPTTester const usd(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, dan, ed},
.pay = 10000'000'000,
.flags = kMptDexFlags});
env.trust(BTC(11'000'000'000'000), ed);
env(pay(gw_, ed, BTC(11'000'000'000'000)));
env.trust(BTC(1'000'000'000'000), bob_);
env(pay(gw_, bob_, BTC(1'000'000'000'000)));
env.trust(BTC(1'000'000'000'000), dan);
env(pay(gw_, dan, BTC(1'000'000'000'000)));
env.close();
nOffers(env, 2'000, bob_, usd(1000000), BTC(1'000'000'000'000));
nOffers(env, 1, dan, usd(1000000), BTC(1'000'000'000'000));
AMM const ammEd(env, ed, usd(9000000), BTC(11'000'000'000'000));
env(offer(alice_, BTC(1'000'000'000'000'000), usd(1'000000000)));
env.require(Balance(alice_, STAmount{BTC, UINT64_C(2050125257867'587), -3}));
env.require(Owners(alice_, 3));
env.require(Balance(bob_, BTC(0)));
env.require(Owners(bob_, 1'002));
env.require(Balance(dan, BTC(1000000000000)));
env.require(Owners(dan, 3));
}
// IOU/MPT, similar to the case above
{
Env env(*this, features);
auto const dan = Account("dan");
auto const ed = Account("ed");
env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
env.close();
env.trust(USD(10000'000'000), alice_);
env(pay(gw_, alice_, USD(10000'000'000)));
env.trust(USD(10000'000'000), bob_);
env(pay(gw_, bob_, USD(10000'000'000)));
env.trust(USD(10000'000'000), carol_);
env(pay(gw_, carol_, USD(10000'000'000)));
env.trust(USD(10000'000'000), dan);
env(pay(gw_, dan, USD(10000'000'000)));
env.trust(USD(10000'000'000), ed);
env(pay(gw_, ed, USD(10000'000'000)));
env.close();
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
env(pay(gw_, ed, btc(11'000'000'000'000)));
env(pay(gw_, bob_, btc(1'000'000'000'000)));
env(pay(gw_, dan, btc(1'000'000'000'000)));
env.close();
nOffers(env, 2'000, bob_, USD(1000000), btc(1'000'000'000'000));
nOffers(env, 1, dan, USD(1000000), btc(1'000'000'000'000));
AMM const ammEd(env, ed, USD(9000000), btc(11'000'000'000'000));
env(offer(alice_, btc(1'000'000'000'000'000), USD(1'000000000)));
env.require(Balance(alice_, btc(2050125628933)));
env.require(Owners(alice_, 3));
env.require(Balance(bob_, btc(0)));
env.require(Owners(bob_, 1'002));
env.require(Balance(dan, btc(1000000000000)));
env.require(Owners(dan, 3));
}
// MPT/MPT, similar to the case above
{
Env env(*this, features);
auto const dan = Account("dan");
auto const ed = Account("ed");
env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
env.close();
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
MPTTester const usd(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_, dan, ed},
.pay = 10000'000'000,
.flags = kMptDexFlags});
env(pay(gw_, ed, btc(11'000'000'000'000)));
env(pay(gw_, bob_, btc(1'000'000'000'000)));
env(pay(gw_, dan, btc(1'000'000'000'000)));
env.close();
nOffers(env, 2'000, bob_, usd(1000000), btc(1'000'000'000'000));
nOffers(env, 1, dan, usd(1000000), btc(1'000'000'000'000));
AMM const ammEd(env, ed, usd(9000000), btc(11'000'000'000'000));
env(offer(alice_, btc(1'000'000'000'000'000), usd(1'000000000)));
env.require(Balance(alice_, btc(2050125257867)));
env.require(Owners(alice_, 3));
env.require(Balance(bob_, btc(0)));
env.require(Owners(bob_, 1'002));
env.require(Balance(dan, btc(1000000000000)));
env.require(Owners(dan, 3));
}
}
void
testConvertAllOfAnAsset(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));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.flags = kMptDexFlags});
env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(10'000)), Ter(temBAD_AMOUNT));
env(pay(alice_, bob_, btc(10'000)),
DeliverMin(btc(-5'000)),
Txflags(tfPartialPayment),
Ter(temBAD_AMOUNT));
env(pay(alice_, bob_, btc(10'000)),
DeliverMin(XRP(5)),
Txflags(tfPartialPayment),
Ter(temBAD_AMOUNT));
env(pay(alice_, bob_, btc(10'000)),
DeliverMin(btc(5'000)),
Txflags(tfPartialPayment),
Ter(tecPATH_DRY));
env(pay(alice_, bob_, btc(10'000)),
DeliverMin(btc(15'000)),
Txflags(tfPartialPayment),
Ter(temBAD_AMOUNT));
env(pay(gw_, carol_, btc(50'000)));
AMM const ammCarol(env, carol_, XRP(10), btc(15'000));
env(pay(alice_, bob_, btc(10'000)),
Paths(XRP),
DeliverMin(btc(7'000)),
Txflags(tfPartialPayment),
Sendmax(XRP(5)),
Ter(tecPATH_PARTIAL));
env.require(
Balance(alice_, drops(10'000'000'000 - (3 * env.current()->fees().base.drops()))));
env.require(Balance(bob_, drops(10'000'000'000 - env.current()->fees().base.drops())));
}
{
Env env(*this, features);
fund(env, gw_, {alice_, bob_}, XRP(10'000));
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(1'100'000)));
AMM const ammBob(env, bob_, XRP(1'000), btc(1'100'000));
env(pay(alice_, alice_, btc(10'000'000)),
Paths(XRP),
DeliverMin(btc(100'000)),
Txflags(tfPartialPayment),
Sendmax(XRP(100)));
env.require(Balance(alice_, btc(100'000)));
}
// IOU/MPT mix, similar to the above case
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
env.close();
auto const usd = issue1(
{.env = env,
.token = "USD",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 3000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1'000'000});
env(pay(gw_, alice_, usd(10'000)));
env(pay(gw_, bob_, usd(10'000)));
env(pay(gw_, bob_, btc(1'200)));
env.close();
AMM const ammBob(env, bob_, usd(1'000), btc(1'100));
env(pay(alice_, alice_, btc(10'000)),
Paths(usd),
DeliverMin(btc(100)),
Txflags(tfPartialPayment),
Sendmax(usd(100)));
env.require(Balance(alice_, btc(100)));
};
testHelper2TokensMix(test);
}
{
Env env(*this, features);
fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {bob_, carol_}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(1'200'000)));
AMM const ammBob(env, bob_, XRP(5'500), btc(1'200'000));
env(pay(alice_, carol_, btc(10'000'000)),
Paths(XRP),
DeliverMin(btc(200'000)),
Txflags(tfPartialPayment),
Sendmax(XRP(1'000)),
Ter(tecPATH_PARTIAL));
env(pay(alice_, carol_, btc(10'000'000)),
Paths(XRP),
DeliverMin(btc(200'000)),
Txflags(tfPartialPayment),
Sendmax(XRP(1'100)));
BEAST_EXPECT(ammBob.expectBalances(XRP(6'600), btc(1'000'000), ammBob.tokens()));
env.require(Balance(carol_, btc(200'000)));
}
// IOU/MPT mix, similar to the above case
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
env.close();
auto const usd = issue1(
{.env = env,
.token = "USD",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 3000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1'000'000});
env(pay(gw_, alice_, usd(100'000)));
env(pay(gw_, bob_, usd(100'000)));
env(pay(gw_, carol_, usd(100'000)));
env(pay(gw_, bob_, btc(1'200)));
env.close();
AMM const ammBob(env, bob_, usd(5'500), btc(1'200));
env(pay(alice_, carol_, btc(10'000)),
Paths(usd),
DeliverMin(btc(200)),
Txflags(tfPartialPayment),
Sendmax(usd(1'000)),
Ter(tecPATH_PARTIAL));
env(pay(alice_, carol_, btc(10'000)),
Paths(usd),
DeliverMin(btc(200)),
Txflags(tfPartialPayment),
Sendmax(usd(1'100)));
BEAST_EXPECT(ammBob.expectBalances(usd(6'600), btc(1'000), ammBob.tokens()));
env.require(Balance(carol_, btc(200)));
};
testHelper2TokensMix(test);
}
{
auto const dan = Account("dan");
Env env(*this, features);
fund(env, gw_, {alice_, bob_, carol_, dan}, XRP(10'000));
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {bob_, carol_, dan}, .flags = kMptDexFlags});
env(pay(gw_, bob_, btc(100'000'000)));
env(pay(gw_, dan, btc(1'100'000'000)));
env(offer(bob_, XRP(100), btc(100'000'000)));
env(offer(bob_, XRP(1'000), btc(100'000'000)));
AMM const ammDan(env, dan, XRP(1'000), btc(1'100'000'000));
env(pay(alice_, carol_, btc(10'000'000'000)),
Paths(XRP),
DeliverMin(btc(200'000'000)),
Txflags(tfPartialPayment),
Sendmax(XRPAmount(200'000'001)));
env.require(Balance(bob_, btc(0)));
env.require(Balance(carol_, btc(200'000'000)));
BEAST_EXPECT(
ammDan.expectBalances(XRPAmount{1'100'000'001}, btc(1000'000000), ammDan.tokens()));
}
}
void
testPayment(FeatureBitset features)
{
testcase("Payment");
using namespace jtx;
Account const becky{"becky"};
Env env(*this, features);
fund(env, gw_, {alice_, becky}, XRP(5'000));
MPTTester const btc(
{.env = env, .issuer = gw_, .holders = {alice_, becky}, .flags = kMptDexFlags});
env(pay(gw_, alice_, btc(500'000)));
env.close();
AMM const ammAlice(env, alice_, XRP(100), btc(140'000));
// becky pays herself BTC (10'000) by consuming part of alice's offer.
// Make sure the payment works if PaymentAuth is not involved.
env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10)));
env.close();
BEAST_EXPECT(
ammAlice.expectBalances(XRPAmount(107'692'308), btc(130'000), ammAlice.tokens()));
// becky decides to require authorization for deposits.
env(fset(becky, asfDepositAuth));
env.close();
// becky pays herself again.
env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10)), Ter(tesSUCCESS));
env.close();
}
void
testPayMPT()
{
// Exercise MPT payments and non-direct XRP payments to an account
// that has the lsfDepositAuth flag set.
testcase("Pay MPT");
using namespace jtx;
Env env(*this);
fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
MPTTester btc(
{.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
env(pay(gw_, alice_, btc(150'000)));
env(pay(gw_, carol_, btc(150'000)));
AMM const ammCarol(env, carol_, btc(100'000), XRPAmount(101));
env(pay(alice_, bob_, btc(50'000)));
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 failedMptPayments = [this, &env, &btc]() {
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 bobBTCBalance{env.balance(bob_, MPT(btc))};
env(pay(alice_, bob_, btc(50'000)), 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(btc(1'000)), Ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(bobXrpBalance == env.balance(bob_, XRP));
BEAST_EXPECT(bobBTCBalance == env.balance(bob_, MPT(btc)));
};
// Test when bob has an XRP balance > base reserve.
failedMptPayments();
// Set bob's XRP balance == base reserve. Also demonstrate that
// bob can make payments while his lsfDepositAuth flag is set.
env(pay(bob_, alice_, btc(25'000)));
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_, MPT(btc)) == btc(25'000));
failedMptPayments();
// 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));
failedMptPayments();
// 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_, btc(50'000)));
env.close();
env(pay(alice_, bob_, drops(1)), Sendmax(btc(1'000)));
env.close();
BEAST_EXPECT(ammCarol.expectBalances(btc(101'000), XRPAmount(100), ammCarol.tokens()));
}
void
testIndividualLock(FeatureBitset features)
{
testcase("Individual Lock");
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);
MPTTester btc(
{.env = env,
.issuer = g1,
.holders = {alice, bob},
.flags = tfMPTCanLock | kMptDexFlags});
env(pay(g1, bob, btc(10)));
env(pay(g1, alice, btc(205)));
env.close();
AMM const ammAlice(env, alice, XRP(500), btc(105));
env.require(Balance(bob, btc(10)));
env.require(Balance(alice, btc(100)));
// Account with MPT unlocked (proving operations normally work)
// can make Payment
env(pay(alice, bob, btc(1)));
// can receive Payment
env(pay(bob, alice, btc(1)));
env.close();
// Lock MPT for bob
btc.set({.holder = bob, .flags = tfMPTLock});
{
// different from IOU. The offer is created but not crossed.
env(offer(bob, btc(5), XRP(25)));
env.close();
BEAST_EXPECT(expectOffers(env, bob, 1, {{{btc(5), XRP(25)}}}));
BEAST_EXPECT(ammAlice.expectBalances(XRP(500), btc(105), ammAlice.tokens()));
}
{
// can not sell assets
env(offer(bob, XRP(1), btc(5)), Ter(tecUNFUNDED_OFFER));
// different from IOU
// can not receive Payment when locked
env(pay(alice, bob, btc(1)), Ter(tecPATH_DRY));
// can not make Payment when locked
env(pay(bob, alice, btc(1)), Ter(tecPATH_DRY));
env.require(Balance(bob, btc(10)));
}
{
// Unlock
btc.set({.holder = bob, .flags = tfMPTUnlock});
env(offer(bob, XRP(1), btc(5)));
env(pay(bob, alice, btc(1)));
env(pay(alice, bob, btc(1)));
env.close();
}
}
void
testGlobalLock(FeatureBitset features)
{
testcase("Global Lock");
using namespace test::jtx;
Env env(*this, features);
Account const 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);
MPTTester const eth(
{.env = env,
.issuer = g1,
.holders = {a1, a2, a3, a4},
.flags = tfMPTCanLock | kMptDexFlags});
MPTTester btc(
{.env = env,
.issuer = g1,
.holders = {a1, a2, a3, a4},
.flags = tfMPTCanLock | kMptDexFlags});
env(pay(g1, a1, eth(1'000)));
env(pay(g1, a2, eth(100)));
env(pay(g1, a3, btc(100)));
env(pay(g1, a4, btc(100)));
env.close();
AMM const ammG1(env, g1, XRP(10'000), eth(100));
env(offer(a1, XRP(10'000), eth(100)), Txflags(tfPassive));
env(offer(a2, eth(100), XRP(10'000)), Txflags(tfPassive));
env.close();
{
// Account without Global Lock (proving operations normally
// work)
// visible offers where taker_pays is unlocked issuer
auto offers = getAccountOffers(env, a2)[jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
// visible offers where taker_gets is unlocked issuer
offers = getAccountOffers(env, a1)[jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
}
{
// Offers/Payments
// assets can be bought on the market
AMM ammA3(env, a3, btc(1), XRP(1));
// assets can be sold on the market
// AMM is bidirectional
env(pay(g1, a2, eth(1)));
env(pay(a2, g1, eth(1)));
env(pay(a2, a1, eth(1)));
env(pay(a1, a2, eth(1)));
ammA3.withdrawAll(std::nullopt);
}
{
// Account with Global Lock
// set Global Lock first
btc.set({.flags = tfMPTLock});
// assets can't be bought on the market
AMM const ammA3(env, a3, btc(1), XRP(1), Ter(tecFROZEN));
// direct issues can be sent
env(pay(g1, a2, btc(1)));
env(pay(a2, g1, btc(1)));
// locked
env(pay(a2, a1, btc(1)), Ter(tecPATH_DRY));
env(pay(a1, a2, btc(1)), Ter(tecPATH_DRY));
}
{
auto offers = getAccountOffers(env, a2)[jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
offers = getAccountOffers(env, a1)[jss::offers];
if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
return;
}
}
void
testOffersWhenLocked(FeatureBitset features)
{
testcase("Offers for Locked MPTs");
using namespace test::jtx;
Env env(*this, features);
Account const 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();
MPTTester btc(
{.env = env,
.issuer = g1,
.holders = {a2, a3, a4},
.flags = tfMPTCanLock | kMptDexFlags});
env(pay(g1, a3, btc(2'000)));
env(pay(g1, a4, btc(2'001)));
env.close();
AMM const ammA3(env, a3, XRP(1'000), btc(1'001));
// removal after successful payment
// test: make a payment with partially consuming offer
env(pay(a2, g1, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1)));
env.close();
BEAST_EXPECT(ammA3.expectBalances(XRP(1'001), btc(1'000), ammA3.tokens()));
// test: someone else creates an offer providing liquidity
env(offer(a4, XRP(999), btc(999)));
env.close();
// The offer consumes AMM offer
BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens()));
// test: AMM is Locked
btc.set({.holder = ammA3.ammAccount(), .flags = tfMPTLock});
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, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1)));
env.close();
// AMM is not consumed
BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens()));
// removal buy successful OfferCreate
// test: lock the new offer
btc.set({.holder = a4, .flags = tfMPTUnlock});
env.close();
// test: can no longer create a crossing offer
env(offer(a2, btc(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));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice, becky, zelda},
.pay = 20'000'000'000,
.flags = kMptDexFlags});
// 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();
static constexpr int kSignerListOwners{2};
env.require(Owners(alice, kSignerListOwners + 0));
Msig const ms{becky, bogie};
// Multisign all AMM transactions
AMM ammAlice(
env,
alice,
XRP(10'000),
btc(10'000),
false,
0,
ammCrtFee(env).drops(),
std::nullopt,
std::nullopt,
ms,
Ter(tesSUCCESS));
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), ammAlice.tokens()));
ammAlice.deposit(alice, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), btc(11'000), IOUAmount{11'000'000, 0}));
ammAlice.withdraw(alice, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(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), btc(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
{
auto test = [&](auto&& issue1, auto&& issue2) {
Env env(*this);
env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
env.close();
auto const eth = issue1(
{.env = env,
.token = "ETH",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1'000'000});
auto const btc = issue2(
{.env = env,
.token = "BTC",
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.limit = 1'000'000});
env(pay(gw_, alice_, btc(50000)));
env(pay(gw_, bob_, btc(50000)));
env(pay(gw_, carol_, btc(50000)));
env(pay(gw_, alice_, eth(50000)));
env(pay(gw_, bob_, eth(50000)));
env(pay(gw_, carol_, eth(50000)));
env.close();
AMM const bobXrpBtc(env, bob_, XRP(1'000), btc(1'000));
AMM const bobBtcEth(env, bob_, btc(1'000), eth(1'000));
// payment path: XRP -> XRP/BTC -> BTC/ETH -> ETH/BTC
env(pay(alice_, carol_, btc(100)),
Path(~btc, ~eth, ~btc),
Sendmax(XRP(200)),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
};
testHelper2TokensMix(test);
}
}
void
testRIPD1373(FeatureBitset features)
{
using namespace jtx;
testcase("RIPD1373");
{
Env env(*this, features);
fund(env, gw_, {alice_, bob_}, XRP(10'000));
MPTTester btc(
{.env = env,
.issuer = bob_,
.holders = {alice_, gw_},
.pay = 100'000'000,
.flags = kMptDexFlags});
MPTTester eth(
{.env = env,
.issuer = bob_,
.holders = {alice_, gw_},
.pay = 100'000'000,
.flags = kMptDexFlags});
AMM const ammXrpBtc(env, bob_, XRP(100), btc(100'000));
env(offer(gw_, XRP(100), btc(100'000)), Txflags(tfPassive));
AMM const ammBtcEth(env, bob_, btc(100'000), eth(100'000));
env(offer(gw_, btc(100'000), eth(100'000)), Txflags(tfPassive));
TestPath const p = [&] {
TestPath result;
result.pushBack(allPathElements(gw_, MPT(btc)));
result.pushBack(cpe(eth.issuanceID()));
return result;
}();
PathSet const paths(p);
env(pay(alice_, alice_, eth(1'000)),
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));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 100'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(100), btc(100));
// payment path: XRP -> XRP/BTC -> BTC/XRP
env(pay(alice_, carol_, XRP(100)),
Path(~MPT(btc), ~XRP),
Txflags(tfNoRippleDirect),
Ter(temBAD_SEND_XRP_PATHS));
}
{
Env env(*this, features);
fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.pay = 100'000,
.flags = kMptDexFlags});
AMM const ammBob(env, bob_, XRP(100), btc(100));
// payment path: XRP -> XRP/BTC -> BTC/XRP
env(pay(alice_, carol_, XRP(100)),
Path(~MPT(btc), ~XRP),
Sendmax(XRP(200)),
Txflags(tfNoRippleDirect),
Ter(temBAD_SEND_XRP_MAX));
}
}
void
testLoop(FeatureBitset features)
{
testcase("test loop");
using namespace jtx;
{
Env env(*this, features);
env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
MPTTester const btc(
{.env = env,
.issuer = gw_,
.holders = {alice_, bob_, carol_},
.flags = kMptDexFlags});
env(pay(gw_, bob_, btc(100'000'000)));
env(pay(gw_, alice_, btc(100'000'000)));
env.close();
AMM const ammBob(env, bob_, XRP(100), btc(100'000'000));
// payment path: BTC -> BTC/XRP -> XRP/BTC
env(pay(alice_, carol_, btc(100'000'000)),
Sendmax(btc(100'000'000)),
Path(~XRP, ~MPT(btc)),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
}
{
auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
Env env(*this, features);
env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
env.close();
auto const btc = issue1(
{.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}});
auto const eth = issue2(
{.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}});
auto const cny = issue3(
{.env = env, .token = "CNY", .issuer = gw_, .holders = {alice_, bob_, carol_}});
env(pay(gw_, bob_, btc(200)));
env(pay(gw_, bob_, eth(200)));
env(pay(gw_, bob_, cny(100)));
env.close();
AMM const ammBobXrpBtc(env, bob_, XRP(100), btc(100));
AMM const ammBobBtcEth(env, bob_, btc(100), eth(100));
AMM const ammBobEthCny(env, bob_, eth(100), cny(100));
// payment path: XRP->XRP/BTC->BTC/ETH->BTC/CNY
env(pay(alice_, carol_, cny(100)),
Sendmax(XRP(100)),
Path(~btc, ~eth, ~btc, ~cny),
Txflags(tfNoRippleDirect),
Ter(temBAD_PATH_LOOP));
};
testHelper3TokensMix(test);
}
}
void
testPaths()
{
pathFindConsumeAll();
viaOffersViaGateway();
receiveMax();
pathFind01();
pathFind02();
pathFind06();
}
void
testFlow()
{
using namespace jtx;
FeatureBitset const all{testableAmendments()};
testFalseDry(all);
testBookStep(all);
testTransferRateNoOwnerFee(all);
testLimitQuality();
testXRPPathLoop();
}
void
testCrossingLimits()
{
using namespace jtx;
FeatureBitset const all{testableAmendments()};
testStepLimit(all);
}
void
testDeliverMin()
{
using namespace jtx;
FeatureBitset const all{testableAmendments()};
testConvertAllOfAnAsset(all);
}
void
testDepositAuth()
{
auto const supported{jtx::testableAmendments()};
testPayment(supported);
testPayMPT();
}
void
testLock()
{
using namespace test::jtx;
auto const sa = testableAmendments();
testIndividualLock(sa);
testGlobalLock(sa);
testOffersWhenLocked(sa);
}
void
testMultisign()
{
using namespace jtx;
auto const all = testableAmendments();
testTxMultisign(all);
}
void
testPayStrand()
{
using namespace jtx;
auto const all = testableAmendments();
testToStrand(all);
testRIPD1373(all);
testLoop(all);
}
void
run() override
{
testOffers();
testPaths();
testFlow();
testCrossingLimits();
testDeliverMin();
testDepositAuth();
testLock();
testMultisign();
testPayStrand();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(AMMExtendedMPT, app, xrpl, 1);
} // namespace xrpl::test