Files
rippled/src/test/app/AMM_test.cpp

7964 lines
314 KiB
C++

#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/sendmax.h>
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/app/paths/AMMContext.h>
#include <xrpld/app/tx/detail/AMMBid.h>
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TER.h>
#include <boost/regex.hpp>
#include <utility>
#include <vector>
namespace ripple {
namespace test {
/**
* Basic tests of AMM that do not use offers.
* Tests incorporating offers are in `AMMExtended_test`.
*/
struct AMM_test : public jtx::AMMTest
{
private:
void
testInstanceCreate()
{
testcase("Instance Create");
using namespace jtx;
// XRP to IOU, with featureSingleAssetVault
testAMM(
[&](AMM& ammAlice, Env&) {
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
},
{},
0,
{},
{testable_amendments() | featureSingleAssetVault});
// XRP to IOU, without featureSingleAssetVault
testAMM(
[&](AMM& ammAlice, Env&) {
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
},
{},
0,
{},
{testable_amendments() - featureSingleAssetVault -
featureLendingProtocol});
// IOU to IOU
testAMM(
[&](AMM& ammAlice, Env&) {
BEAST_EXPECT(ammAlice.expectBalances(
USD(20'000), BTC(0.5), IOUAmount{100, 0}));
},
{{USD(20'000), BTC(0.5)}});
// IOU to IOU + transfer fee
{
Env env{*this};
fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
env(rate(gw, 1.25));
env.close();
// no transfer fee on create
AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
BEAST_EXPECT(ammAlice.expectBalances(
USD(20'000), BTC(0.5), IOUAmount{100, 0}));
BEAST_EXPECT(expectHolding(env, alice, USD(0)));
BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
}
// Require authorization is set, account is authorized
{
Env env{*this};
env.fund(XRP(30'000), gw, alice);
env.close();
env(fset(gw, asfRequireAuth));
env(trust(alice, gw["USD"](30'000), 0));
env(trust(gw, alice["USD"](0), tfSetfAuth));
env.close();
env(pay(gw, alice, USD(10'000)));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
}
// Cleared global freeze
{
Env env{*this};
env.fund(XRP(30'000), gw, alice);
env.close();
env.trust(USD(30'000), alice);
env.close();
env(pay(gw, alice, USD(10'000)));
env.close();
env(fset(gw, asfGlobalFreeze));
env.close();
AMM ammAliceFail(
env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
env(fclear(gw, asfGlobalFreeze));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
}
// Trading fee
testAMM(
[&](AMM& amm, Env&) {
BEAST_EXPECT(amm.expectTradingFee(1'000));
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
},
std::nullopt,
1'000);
// Make sure asset comparison works.
BEAST_EXPECT(
STIssue(sfAsset, STAmount(XRP(2'000)).issue()) ==
STIssue(sfAsset, STAmount(XRP(2'000)).issue()));
BEAST_EXPECT(
STIssue(sfAsset, STAmount(XRP(2'000)).issue()) !=
STIssue(sfAsset, STAmount(USD(2'000)).issue()));
}
void
testInvalidInstance()
{
testcase("Invalid Instance");
using namespace jtx;
// Can't have both XRP tokens
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env, alice, XRP(10'000), XRP(10'000), ter(temBAD_AMM_TOKENS));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Can't have both tokens the same IOU
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env, alice, USD(10'000), USD(10'000), ter(temBAD_AMM_TOKENS));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Can't have zero or negative amounts
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(env, alice, XRP(0), USD(10'000), ter(temBAD_AMOUNT));
BEAST_EXPECT(!ammAlice.ammExists());
AMM ammAlice1(env, alice, XRP(10'000), USD(0), ter(temBAD_AMOUNT));
BEAST_EXPECT(!ammAlice1.ammExists());
AMM ammAlice2(
env, alice, XRP(10'000), USD(-10'000), ter(temBAD_AMOUNT));
BEAST_EXPECT(!ammAlice2.ammExists());
AMM ammAlice3(
env, alice, XRP(-10'000), USD(10'000), ter(temBAD_AMOUNT));
BEAST_EXPECT(!ammAlice3.ammExists());
}
// Bad currency
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env, alice, XRP(10'000), BAD(10'000), ter(temBAD_CURRENCY));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Insufficient IOU balance
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env, alice, XRP(10'000), USD(40'000), ter(tecUNFUNDED_AMM));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Insufficient XRP balance
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env, alice, XRP(40'000), USD(10'000), ter(tecUNFUNDED_AMM));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Invalid trading fee
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env,
alice,
XRP(10'000),
USD(10'000),
false,
65'001,
10,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_FEE));
BEAST_EXPECT(!ammAlice.ammExists());
}
// AMM already exists
testAMM([&](AMM& ammAlice, Env& env) {
AMM ammCarol(
env, carol, XRP(10'000), USD(10'000), ter(tecDUPLICATE));
});
// Invalid flags
{
Env env{*this};
fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
AMM ammAlice(
env,
alice,
XRP(10'000),
USD(10'000),
false,
0,
10,
tfWithdrawAll,
std::nullopt,
std::nullopt,
ter(temINVALID_FLAG));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Invalid Account
{
Env env{*this};
Account bad("bad");
env.memoize(bad);
AMM ammAlice(
env,
bad,
XRP(10'000),
USD(10'000),
false,
0,
10,
std::nullopt,
seq(1),
std::nullopt,
ter(terNO_ACCOUNT));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Require authorization is set
{
Env env{*this};
env.fund(XRP(30'000), gw, alice);
env.close();
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, alice["USD"](30'000)));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecNO_AUTH));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Globally frozen
{
Env env{*this};
env.fund(XRP(30'000), gw, alice);
env.close();
env(fset(gw, asfGlobalFreeze));
env.close();
env(trust(gw, alice["USD"](30'000)));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Individually frozen
{
Env env{*this};
env.fund(XRP(30'000), gw, alice);
env.close();
env(trust(gw, alice["USD"](30'000)));
env.close();
env(trust(gw, alice["USD"](0), tfSetFreeze));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
BEAST_EXPECT(!ammAlice.ammExists());
}
// Insufficient reserve, XRP/IOU
{
Env env(*this);
auto const starting_xrp =
XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4;
env.fund(starting_xrp, gw);
env.fund(starting_xrp, alice);
env.trust(USD(2'000), alice);
env.close();
env(pay(gw, alice, USD(2'000)));
env.close();
env(offer(alice, XRP(101), USD(100)));
env(offer(alice, XRP(102), USD(100)));
AMM ammAlice(
env, alice, XRP(1'000), USD(1'000), ter(tecUNFUNDED_AMM));
}
// Insufficient reserve, IOU/IOU
{
Env env(*this);
auto const starting_xrp =
reserve(env, 4) + env.current()->fees().base * 5;
env.fund(starting_xrp, gw);
env.fund(starting_xrp, alice);
env.trust(USD(2'000), alice);
env.trust(EUR(2'000), alice);
env.close();
env(pay(gw, alice, USD(2'000)));
env(pay(gw, alice, EUR(2'000)));
env.close();
env(offer(alice, EUR(101), USD(100)));
env(offer(alice, EUR(102), USD(100)));
AMM ammAlice(
env, alice, EUR(1'000), USD(1'000), ter(tecINSUF_RESERVE_LINE));
}
// Insufficient fee
{
Env env(*this);
fund(env, gw, {alice}, XRP(2'000), {USD(2'000), EUR(2'000)});
AMM ammAlice(
env,
alice,
EUR(1'000),
USD(1'000),
false,
0,
ammCrtFee(env).drops() - 1,
std::nullopt,
std::nullopt,
std::nullopt,
ter(telINSUF_FEE_P));
}
// AMM with LPTokens
// AMM with one LPToken from another AMM.
testAMM([&](AMM& ammAlice, Env& env) {
fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
AMM ammAMMToken(
env,
alice,
EUR(10'000),
STAmount{ammAlice.lptIssue(), 1'000'000},
ter(tecAMM_INVALID_TOKENS));
AMM ammAMMToken1(
env,
alice,
STAmount{ammAlice.lptIssue(), 1'000'000},
EUR(10'000),
ter(tecAMM_INVALID_TOKENS));
});
// AMM with two LPTokens from other AMMs.
testAMM([&](AMM& ammAlice, Env& env) {
fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
auto const token1 = ammAlice.lptIssue();
auto const token2 = ammAlice1.lptIssue();
AMM ammAMMTokens(
env,
alice,
STAmount{token1, 1'000'000},
STAmount{token2, 1'000'000},
ter(tecAMM_INVALID_TOKENS));
});
// Issuer has DefaultRipple disabled
{
Env env(*this);
env.fund(XRP(30'000), gw);
env(fclear(gw, asfDefaultRipple));
AMM ammGw(env, gw, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
env.fund(XRP(30'000), alice);
env.trust(USD(30'000), alice);
env(pay(gw, alice, USD(30'000)));
AMM ammAlice(
env, alice, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
Account const gw1("gw1");
env.fund(XRP(30'000), gw1);
env(fclear(gw1, asfDefaultRipple));
env.trust(USD(30'000), gw1);
env(pay(gw, gw1, USD(30'000)));
auto const USD1 = gw1["USD"];
AMM ammGwGw1(env, gw, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
env.trust(USD1(30'000), alice);
env(pay(gw1, alice, USD1(30'000)));
AMM ammAlice1(
env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
}
}
void
testInvalidDeposit(FeatureBitset features)
{
testcase("Invalid Deposit");
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
ammAlice.deposit(
alice,
1'000'000,
std::nullopt,
tfWithdrawAll,
ter(temINVALID_FLAG));
// Invalid options
std::vector<std::tuple<
std::optional<std::uint32_t>,
std::optional<std::uint32_t>,
std::optional<STAmount>,
std::optional<STAmount>,
std::optional<STAmount>,
std::optional<std::uint16_t>>>
invalidOptions = {
// flags, tokens, asset1In, asset2in, EPrice, tfee
{tfLPToken,
1'000,
std::nullopt,
USD(100),
std::nullopt,
std::nullopt},
{tfLPToken,
1'000,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt},
{tfLPToken,
1'000,
std::nullopt,
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt},
{tfLPToken,
std::nullopt,
USD(100),
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt},
{tfLPToken,
1'000,
XRP(100),
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt},
{tfLPToken,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
1'000},
{tfSingleAsset,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt},
{tfSingleAsset,
std::nullopt,
std::nullopt,
USD(100),
std::nullopt,
std::nullopt},
{tfSingleAsset,
std::nullopt,
std::nullopt,
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt},
{tfSingleAsset,
std::nullopt,
USD(100),
std::nullopt,
std::nullopt,
1'000},
{tfTwoAsset,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt},
{tfTwoAsset,
std::nullopt,
XRP(100),
USD(100),
STAmount{USD, 1, -1},
std::nullopt},
{tfTwoAsset,
std::nullopt,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt},
{tfTwoAsset,
std::nullopt,
XRP(100),
USD(100),
std::nullopt,
1'000},
{tfTwoAsset,
std::nullopt,
std::nullopt,
USD(100),
STAmount{USD, 1, -1},
std::nullopt},
{tfOneAssetLPToken,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt},
{tfOneAssetLPToken,
std::nullopt,
XRP(100),
USD(100),
std::nullopt,
std::nullopt},
{tfOneAssetLPToken,
std::nullopt,
XRP(100),
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt},
{tfOneAssetLPToken,
1'000,
XRP(100),
std::nullopt,
std::nullopt,
1'000},
{tfLimitLPToken,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt},
{tfLimitLPToken,
1'000,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt},
{tfLimitLPToken,
std::nullopt,
USD(100),
XRP(100),
std::nullopt,
std::nullopt},
{tfLimitLPToken,
std::nullopt,
XRP(100),
std::nullopt,
STAmount{USD, 1, -1},
1'000},
{tfTwoAssetIfEmpty,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
1'000},
{tfTwoAssetIfEmpty,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt},
{tfTwoAssetIfEmpty,
std::nullopt,
XRP(100),
USD(100),
STAmount{USD, 1, -1},
std::nullopt},
{tfTwoAssetIfEmpty | tfLPToken,
std::nullopt,
XRP(100),
USD(100),
STAmount{USD, 1, -1},
std::nullopt}};
for (auto const& it : invalidOptions)
{
ammAlice.deposit(
alice,
std::get<1>(it),
std::get<2>(it),
std::get<3>(it),
std::get<4>(it),
std::get<0>(it),
std::nullopt,
std::nullopt,
std::get<5>(it),
ter(temMALFORMED));
}
{
// bad preflight1
Json::Value jv = Json::objectValue;
jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::AMMDeposit;
jv[jss::Asset] =
STIssue(sfAsset, XRP).getJson(JsonOptions::none);
jv[jss::Asset2] =
STIssue(sfAsset, USD).getJson(JsonOptions::none);
jv[jss::Fee] = "-1";
env(jv, ter(temBAD_FEE));
}
// Invalid tokens
ammAlice.deposit(
alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
ammAlice.deposit(
alice,
IOUAmount{-1},
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
{
Json::Value jv = Json::objectValue;
jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::AMMDeposit;
jv[jss::Asset] =
STIssue(sfAsset, XRP).getJson(JsonOptions::none);
jv[jss::Asset2] =
STIssue(sfAsset, USD).getJson(JsonOptions::none);
jv[jss::LPTokenOut] =
USD(100).value().getJson(JsonOptions::none);
jv[jss::Flags] = tfLPToken;
env(jv, ter(temBAD_AMM_TOKENS));
}
// Invalid trading fee
ammAlice.deposit(
carol,
std::nullopt,
XRP(200),
USD(200),
std::nullopt,
tfTwoAssetIfEmpty,
std::nullopt,
std::nullopt,
10'000,
ter(temBAD_FEE));
// Invalid tokens - bogus currency
{
auto const iss1 = Issue{Currency(0xabc), gw.id()};
auto const iss2 = Issue{Currency(0xdef), gw.id()};
ammAlice.deposit(
alice,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
{{iss1, iss2}},
std::nullopt,
std::nullopt,
ter(terNO_AMM));
}
// Depositing mismatched token, invalid Asset1In.issue
ammAlice.deposit(
alice,
GBP(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Depositing mismatched token, invalid Asset2In.issue
ammAlice.deposit(
alice,
USD(100),
GBP(100),
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Depositing mismatched token, Asset1In.issue == Asset2In.issue
ammAlice.deposit(
alice,
USD(100),
USD(100),
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Invalid amount value
ammAlice.deposit(
alice,
USD(0),
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
ammAlice.deposit(
alice,
USD(-1'000),
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
ammAlice.deposit(
alice,
USD(10),
std::nullopt,
USD(-1),
std::nullopt,
ter(temBAD_AMOUNT));
// Bad currency
ammAlice.deposit(
alice,
BAD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_CURRENCY));
// Invalid Account
Account bad("bad");
env.memoize(bad);
ammAlice.deposit(
bad,
1'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
seq(1),
std::nullopt,
ter(terNO_ACCOUNT));
// Invalid AMM
ammAlice.deposit(
alice,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
{{USD, GBP}},
std::nullopt,
std::nullopt,
ter(terNO_AMM));
// Single deposit: 100000 tokens worth of USD
// Amount to deposit exceeds Max
ammAlice.deposit(
carol,
100'000,
USD(200),
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Single deposit: 100000 tokens worth of XRP
// Amount to deposit exceeds Max
ammAlice.deposit(
carol,
100'000,
XRP(200),
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Deposit amount is invalid
// Calculated amount to deposit is 98,000,000
ammAlice.deposit(
alice,
USD(0),
std::nullopt,
STAmount{USD, 1, -1},
std::nullopt,
ter(tecUNFUNDED_AMM));
// Calculated amount is 0
ammAlice.deposit(
alice,
USD(0),
std::nullopt,
STAmount{USD, 2'000, -6},
std::nullopt,
ter(tecAMM_FAILED));
// Deposit non-empty AMM
ammAlice.deposit(
carol,
XRP(100),
USD(100),
std::nullopt,
tfTwoAssetIfEmpty,
ter(tecAMM_NOT_EMPTY));
});
// Tiny deposit
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const enabledv1_3 =
env.current()->rules().enabled(fixAMMv1_3);
auto const err =
!enabledv1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS);
// Pre-amendment XRP deposit side is rounded to 0
// and deposit fails.
// Post-amendment XRP deposit side is rounded to 1
// and deposit succeeds.
ammAlice.deposit(
carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err);
// Pre/post-amendment LPTokens is rounded to 0 and deposit
// fails with tecAMM_INVALID_TOKENS.
ammAlice.deposit(
carol,
STAmount{USD, 1, -12},
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
},
std::nullopt,
0,
std::nullopt,
{features, features - fixAMMv1_3});
// Invalid AMM
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.withdrawAll(alice);
ammAlice.deposit(
alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
});
// Globally frozen asset
testAMM(
[&](AMM& ammAlice, Env& env) {
env(fset(gw, asfGlobalFreeze));
if (!features[featureAMMClawback])
// If the issuer set global freeze, the holder still can
// deposit the other non-frozen token when AMMClawback is
// not enabled.
ammAlice.deposit(carol, XRP(100));
else
// If the issuer set global freeze, the holder cannot
// deposit the other non-frozen token when AMMClawback is
// enabled.
ammAlice.deposit(
carol,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
XRP(100),
USD(100),
std::nullopt,
std::nullopt,
ter(tecFROZEN));
},
std::nullopt,
0,
std::nullopt,
{features});
// Individually frozen (AMM) account
testAMM(
[&](AMM& ammAlice, Env& env) {
env(trust(gw, carol["USD"](0), tfSetFreeze));
env.close();
if (!features[featureAMMClawback])
// Can deposit non-frozen token if AMMClawback is not
// enabled
ammAlice.deposit(carol, XRP(100));
else
// Cannot deposit non-frozen token if the other token is
// frozen when AMMClawback is enabled
ammAlice.deposit(
carol,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
env(trust(gw, carol["USD"](0), tfClearFreeze));
// Individually frozen AMM
env(trust(
gw,
STAmount{
Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
tfSetFreeze));
env.close();
// Can deposit non-frozen token
ammAlice.deposit(carol, XRP(100));
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
},
std::nullopt,
0,
std::nullopt,
{features});
// Individually frozen (AMM) account with IOU/IOU AMM
testAMM(
[&](AMM& ammAlice, Env& env) {
env(trust(gw, carol["USD"](0), tfSetFreeze));
env(trust(gw, carol["BTC"](0), tfSetFreeze));
env.close();
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
env(trust(gw, carol["USD"](0), tfClearFreeze));
// Individually frozen AMM
env(trust(
gw,
STAmount{
Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
tfSetFreeze));
env.close();
// Cannot deposit non-frozen token
ammAlice.deposit(
carol,
1'000'000,
std::nullopt,
std::nullopt,
ter(tecFROZEN));
ammAlice.deposit(
carol,
USD(100),
BTC(0.01),
std::nullopt,
std::nullopt,
ter(tecFROZEN));
},
{{USD(20'000), BTC(0.5)}});
// Deposit unauthorized token.
{
Env env(*this, features);
env.fund(XRP(1000), gw, alice, bob);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth));
env(trust(alice, gw["USD"](20)));
env.close();
env(pay(gw, alice, gw["USD"](10)));
env.close();
env(trust(gw, bob["USD"](100)));
env.close();
AMM amm(env, alice, XRP(10), gw["USD"](10), ter(tesSUCCESS));
env.close();
if (features[featureAMMClawback])
// if featureAMMClawback is enabled, bob can not deposit XRP
// because he's not authorized to hold the paired token
// gw["USD"].
amm.deposit(
bob,
XRP(10),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecNO_AUTH));
else
amm.deposit(
bob,
XRP(10),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tesSUCCESS));
}
// Insufficient XRP balance
testAMM([&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
env.close();
// Adds LPT trustline
ammAlice.deposit(bob, XRP(10));
ammAlice.deposit(
bob,
XRP(1'000),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
// Insufficient USD balance
testAMM([&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
env.close();
ammAlice.deposit(
bob,
USD(1'001),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
// Insufficient USD balance by tokens
testAMM([&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
env.close();
ammAlice.deposit(
bob,
10'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
// Insufficient XRP balance by tokens
testAMM([&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
env.trust(USD(100'000), bob);
env.close();
env(pay(gw, bob, USD(90'000)));
env.close();
ammAlice.deposit(
bob,
10'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
// Insufficient reserve, XRP/IOU
{
Env env(*this);
auto const starting_xrp =
reserve(env, 4) + env.current()->fees().base * 4;
env.fund(XRP(10'000), gw);
env.fund(XRP(10'000), alice);
env.fund(starting_xrp, carol);
env.trust(USD(2'000), alice);
env.trust(USD(2'000), carol);
env.close();
env(pay(gw, alice, USD(2'000)));
env(pay(gw, carol, USD(2'000)));
env.close();
env(offer(carol, XRP(100), USD(101)));
env(offer(carol, XRP(100), USD(102)));
AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
ammAlice.deposit(
carol,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecINSUF_RESERVE_LINE));
env(offer(carol, XRP(100), USD(103)));
ammAlice.deposit(
carol,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecINSUF_RESERVE_LINE));
}
// Insufficient reserve, IOU/IOU
{
Env env(*this);
auto const starting_xrp =
reserve(env, 4) + env.current()->fees().base * 4;
env.fund(XRP(10'000), gw);
env.fund(XRP(10'000), alice);
env.fund(starting_xrp, carol);
env.trust(USD(2'000), alice);
env.trust(EUR(2'000), alice);
env.trust(USD(2'000), carol);
env.trust(EUR(2'000), carol);
env.close();
env(pay(gw, alice, USD(2'000)));
env(pay(gw, alice, EUR(2'000)));
env(pay(gw, carol, USD(2'000)));
env(pay(gw, carol, EUR(2'000)));
env.close();
env(offer(carol, XRP(100), USD(101)));
env(offer(carol, XRP(100), USD(102)));
AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
ammAlice.deposit(
carol,
XRP(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecINSUF_RESERVE_LINE));
}
// Invalid min
testAMM([&](AMM& ammAlice, Env& env) {
// min tokens can't be <= zero
ammAlice.deposit(
carol, 0, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
ammAlice.deposit(
carol, -1, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
ammAlice.deposit(
carol,
0,
XRP(100),
USD(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// min amounts can't be <= zero
ammAlice.deposit(
carol,
1'000,
XRP(0),
USD(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
ammAlice.deposit(
carol,
1'000,
XRP(100),
USD(-1),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
// min amount bad currency
ammAlice.deposit(
carol,
1'000,
XRP(100),
BAD(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_CURRENCY));
// min amount bad token pair
ammAlice.deposit(
carol,
1'000,
XRP(100),
XRP(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
ammAlice.deposit(
carol,
1'000,
XRP(100),
GBP(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
});
// Min deposit
testAMM([&](AMM& ammAlice, Env& env) {
// Equal deposit by tokens
ammAlice.deposit(
carol,
1'000'000,
XRP(1'000),
USD(1'001),
std::nullopt,
tfLPToken,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
ammAlice.deposit(
carol,
1'000'000,
XRP(1'001),
USD(1'000),
std::nullopt,
tfLPToken,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Equal deposit by asset
ammAlice.deposit(
carol,
100'001,
XRP(100),
USD(100),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Single deposit by asset
ammAlice.deposit(
carol,
488'090,
XRP(1'000),
std::nullopt,
std::nullopt,
tfSingleAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
});
// Equal deposit, tokens rounded to 0
testAMM([&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.tokens = IOUAmount{1, -12},
.err = ter(tecAMM_INVALID_TOKENS)});
});
// Equal deposit limit, tokens rounded to 0
testAMM(
[&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.asset1In = STAmount{USD, 1, -15},
.asset2In = XRPAmount{1},
.err = ter(tecAMM_INVALID_TOKENS)});
},
{.pool = {{USD(1'000'000), XRP(1'000'000)}},
.features = {features - fixAMMv1_3}});
testAMM([&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.asset1In = STAmount{USD, 1, -15},
.asset2In = XRPAmount{1},
.err = ter(tecAMM_INVALID_TOKENS)});
});
// Single deposit by asset, tokens rounded to 0
testAMM([&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.asset1In = STAmount{USD, 1, -15},
.err = ter(tecAMM_INVALID_TOKENS)});
});
// Single deposit by tokens, tokens rounded to 0
testAMM([&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.tokens = IOUAmount{1, -10},
.asset1In = STAmount{USD, 1, -15},
.err = ter(tecAMM_INVALID_TOKENS)});
});
// Single deposit with eprice, tokens rounded to 0
testAMM([&](AMM& amm, Env& env) {
amm.deposit(DepositArg{
.asset1In = STAmount{USD, 1, -15},
.maxEP = STAmount{USD, 1, -1},
.err = ter(tecAMM_INVALID_TOKENS)});
});
}
void
testDeposit()
{
testcase("Deposit");
using namespace jtx;
auto const all = testable_amendments();
// Equal deposit: 1000000 tokens, 10% of the current pool
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base;
ammAlice.deposit(carol, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
// 30,000 less deposited 1,000
BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
// 30,000 less deposited 1,000 and 10 drops tx fee
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRPAmount{29'000'000'000 - baseFee}));
});
// equal asset deposit: unit test to exercise the rounding-down of
// LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
// The LPTokens need to have 16 significant digits and a fractional part
for (Number const deltaLPTokens :
{Number{UINT64_C(100000'0000000009), -10},
Number{UINT64_C(100000'0000000001), -10}})
{
testAMM([&](AMM& ammAlice, Env& env) {
// initial LPToken balance
IOUAmount const initLPToken = ammAlice.getLPTokensBalance();
IOUAmount const newLPTokens{
deltaLPTokens.mantissa(), deltaLPTokens.exponent()};
// carol performs a two-asset deposit
ammAlice.deposit(
DepositArg{.account = carol, .tokens = newLPTokens});
IOUAmount const finalLPToken = ammAlice.getLPTokensBalance();
// Change in behavior due to rounding down of LPTokens:
// there is a decrease in the observed return of LPTokens --
// Inputs Number{UINT64_C(100000'0000000001), -10} and
// Number{UINT64_C(100000'0000000009), -10} are both rounded
// down to 1e5
BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5}));
BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens);
// fraction of newLPTokens/(existing LPToken balance). The
// existing LPToken balance is 1e7
Number const fr = deltaLPTokens / 1e7;
// The below equations are based on Equation 1, 2 from XLS-30d
// specification, Section: 2.3.1.2
Number const deltaXRP = fr * 1e10;
Number const deltaUSD = fr * 1e4;
STAmount const depositUSD =
STAmount{USD, deltaUSD.mantissa(), deltaUSD.exponent()};
STAmount const depositXRP =
STAmount{XRP, deltaXRP.mantissa(), deltaXRP.exponent()};
// initial LPTokens (1e7) + newLPTokens
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000) + depositXRP,
USD(10'000) + depositUSD,
IOUAmount{1, 7} + newLPTokens));
// 30,000 less deposited depositUSD
BEAST_EXPECT(
expectHolding(env, carol, USD(30'000) - depositUSD));
// 30,000 less deposited depositXRP and 10 drops tx fee
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRP(30'000) - depositXRP - txfee(env, 1)));
});
}
// Equal limit deposit: deposit USD100 and XRP proportionally
// to the pool composition not to exceed 100XRP. If the amount
// exceeds 100XRP then deposit 100XRP and USD proportionally
// to the pool composition not to exceed 100USD. Fail if exceeded.
// Deposit 100USD/100XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(100), XRP(100));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
});
// Equal limit deposit.
// Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(200), XRP(100));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
});
// Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(100), XRP(200));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
});
// Single deposit: 1000 USD
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'999'99999999999), -11},
IOUAmount{10'488'088'48170151, -8}));
});
// Single deposit: 1000 XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, XRP(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
});
// Single deposit: 100000 tokens worth of USD
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 100000, USD(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0}));
});
// Single deposit: 100000 tokens worth of XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 100'000, XRP(205));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0}));
});
// Single deposit with EP not exceeding specified:
// 100USD with EP not to exceed 0.1 (AssetIn/TokensOut)
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(
carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'999'99999999999), -11},
IOUAmount{10'488'088'48170151, -8}));
});
// Single deposit with EP not exceeding specified:
// 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(
carol, USD(100), std::nullopt, STAmount{USD, 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, 10'080'16, -2},
IOUAmount{10'040'000, 0}));
});
// Single deposit with EP not exceeding specified:
// 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(
carol, USD(0), std::nullopt, STAmount{USD, 2004, -6});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, 10'080'16, -2},
IOUAmount{10'040'000, 0}));
});
// IOU to IOU + transfer fee
{
Env env{*this};
fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
env(rate(gw, 1.25));
env.close();
AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
BEAST_EXPECT(ammAlice.expectBalances(
USD(20'000), BTC(0.5), IOUAmount{100, 0}));
BEAST_EXPECT(expectHolding(env, alice, USD(0)));
BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
// no transfer fee on deposit
ammAlice.deposit(carol, 10);
BEAST_EXPECT(ammAlice.expectBalances(
USD(22'000), BTC(0.55), IOUAmount{110, 0}));
BEAST_EXPECT(expectHolding(env, carol, USD(0)));
BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
}
// Tiny deposits
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, IOUAmount{1, -3});
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'000'000'001},
STAmount{USD, UINT64_C(10'000'000001), -6},
IOUAmount{10'000'000'001, -3}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1, -3}));
});
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, XRPAmount{1});
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'000'000'001},
USD(10'000),
IOUAmount{1'000'000'000049999, -8}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{49999, -8}));
});
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, STAmount{USD, 1, -10});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'000'00000000008), -11},
IOUAmount{10'000'000'00000004, -8}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4, -8}));
});
// Issuer create/deposit
for (auto const& feat : {all, all - fixAMMv1_3})
{
Env env(*this, feat);
env.fund(XRP(30000), gw);
AMM ammGw(env, gw, XRP(10'000), USD(10'000));
BEAST_EXPECT(
ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens()));
ammGw.deposit(gw, 1'000'000);
BEAST_EXPECT(ammGw.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
ammGw.deposit(gw, USD(1'000));
BEAST_EXPECT(ammGw.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(11'999'99999999998), -11},
IOUAmount{11'489'125'29307605, -8}));
}
// Issuer deposit
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(gw, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
ammAlice.deposit(gw, USD(1'000));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(11'999'99999999998), -11},
IOUAmount{11'489'125'29307605, -8}));
});
// Min deposit
testAMM([&](AMM& ammAlice, Env& env) {
// Equal deposit by tokens
ammAlice.deposit(
carol,
1'000'000,
XRP(1'000),
USD(1'000),
std::nullopt,
tfLPToken,
std::nullopt,
std::nullopt);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
});
testAMM([&](AMM& ammAlice, Env& env) {
// Equal deposit by asset
ammAlice.deposit(
carol,
1'000'000,
XRP(1'000),
USD(1'000),
std::nullopt,
tfTwoAsset,
std::nullopt,
std::nullopt);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
});
testAMM([&](AMM& ammAlice, Env& env) {
// Single deposit by asset
ammAlice.deposit(
carol,
488'088,
XRP(1'000),
std::nullopt,
std::nullopt,
tfSingleAsset,
std::nullopt,
std::nullopt);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
});
testAMM([&](AMM& ammAlice, Env& env) {
// Single deposit by asset
ammAlice.deposit(
carol,
488'088,
USD(1'000),
std::nullopt,
std::nullopt,
tfSingleAsset,
std::nullopt,
std::nullopt);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'999'99999999999), -11},
IOUAmount{10'488'088'48170151, -8}));
});
}
void
testInvalidWithdraw()
{
testcase("Invalid Withdraw");
using namespace jtx;
auto const all = testable_amendments();
testAMM(
[&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.err = ter(tecAMM_BALANCE),
};
ammAlice.withdraw(args);
},
{{XRP(99), USD(99)}});
testAMM(
[&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = USD(100),
.err = ter(tecAMM_BALANCE),
};
ammAlice.withdraw(args);
},
{{XRP(99), USD(99)}});
{
Env env{*this};
env.fund(XRP(30'000), gw, alice, bob);
env.close();
env(fset(gw, asfRequireAuth));
env.close();
env(trust(alice, gw["USD"](30'000), 0));
env(trust(gw, alice["USD"](0), tfSetfAuth));
// Bob trusts Gateway to owe him USD...
env(trust(bob, gw["USD"](30'000), 0));
// ...but Gateway does not authorize Bob to hold its USD.
env.close();
env(pay(gw, alice, USD(10'000)));
env.close();
AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
WithdrawArg args{
.account = bob,
.asset1Out = USD(100),
.err = ter(tecNO_AUTH),
};
ammAlice.withdraw(args);
}
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
ammAlice.withdraw(
alice,
1'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
tfBurnable,
std::nullopt,
std::nullopt,
ter(temINVALID_FLAG));
ammAlice.withdraw(
alice,
1'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
tfTwoAssetIfEmpty,
std::nullopt,
std::nullopt,
ter(temINVALID_FLAG));
// Invalid options
std::vector<std::tuple<
std::optional<std::uint32_t>,
std::optional<STAmount>,
std::optional<STAmount>,
std::optional<IOUAmount>,
std::optional<std::uint32_t>,
NotTEC>>
invalidOptions = {
// tokens, asset1Out, asset2Out, EPrice, flags, ter
{std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
temMALFORMED},
{std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
tfSingleAsset | tfTwoAsset,
temMALFORMED},
{1'000,
std::nullopt,
std::nullopt,
std::nullopt,
tfWithdrawAll,
temMALFORMED},
{std::nullopt,
USD(0),
XRP(100),
std::nullopt,
tfWithdrawAll | tfLPToken,
temMALFORMED},
{std::nullopt,
std::nullopt,
USD(100),
std::nullopt,
tfWithdrawAll,
temMALFORMED},
{std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
tfWithdrawAll | tfOneAssetWithdrawAll,
temMALFORMED},
{std::nullopt,
USD(100),
std::nullopt,
std::nullopt,
tfWithdrawAll,
temMALFORMED},
{std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
tfOneAssetWithdrawAll,
temMALFORMED},
{1'000,
std::nullopt,
USD(100),
std::nullopt,
std::nullopt,
temMALFORMED},
{std::nullopt,
std::nullopt,
std::nullopt,
IOUAmount{250, 0},
tfWithdrawAll,
temMALFORMED},
{1'000,
std::nullopt,
std::nullopt,
IOUAmount{250, 0},
std::nullopt,
temMALFORMED},
{std::nullopt,
std::nullopt,
USD(100),
IOUAmount{250, 0},
std::nullopt,
temMALFORMED},
{std::nullopt,
XRP(100),
USD(100),
IOUAmount{250, 0},
std::nullopt,
temMALFORMED},
{1'000,
XRP(100),
USD(100),
std::nullopt,
std::nullopt,
temMALFORMED},
{std::nullopt,
XRP(100),
USD(100),
std::nullopt,
tfWithdrawAll,
temMALFORMED}};
for (auto const& it : invalidOptions)
{
ammAlice.withdraw(
alice,
std::get<0>(it),
std::get<1>(it),
std::get<2>(it),
std::get<3>(it),
std::get<4>(it),
std::nullopt,
std::nullopt,
ter(std::get<5>(it)));
}
// Invalid tokens
ammAlice.withdraw(
alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
ammAlice.withdraw(
alice,
IOUAmount{-1},
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Mismatched token, invalid Asset1Out issue
ammAlice.withdraw(
alice,
GBP(100),
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Mismatched token, invalid Asset2Out issue
ammAlice.withdraw(
alice,
USD(100),
GBP(100),
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Mismatched token, Asset1Out.issue == Asset2Out.issue
ammAlice.withdraw(
alice,
USD(100),
USD(100),
std::nullopt,
ter(temBAD_AMM_TOKENS));
// Invalid amount value
ammAlice.withdraw(
alice, USD(0), std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
ammAlice.withdraw(
alice,
USD(-100),
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
ammAlice.withdraw(
alice,
USD(10),
std::nullopt,
IOUAmount{-1},
ter(temBAD_AMOUNT));
// Invalid amount/token value, withdraw all tokens from one side
// of the pool.
ammAlice.withdraw(
alice,
USD(10'000),
std::nullopt,
std::nullopt,
ter(tecAMM_BALANCE));
ammAlice.withdraw(
alice,
XRP(10'000),
std::nullopt,
std::nullopt,
ter(tecAMM_BALANCE));
ammAlice.withdraw(
alice,
std::nullopt,
USD(0),
std::nullopt,
std::nullopt,
tfOneAssetWithdrawAll,
std::nullopt,
std::nullopt,
ter(tecAMM_BALANCE));
// Bad currency
ammAlice.withdraw(
alice,
BAD(100),
std::nullopt,
std::nullopt,
ter(temBAD_CURRENCY));
// Invalid Account
Account bad("bad");
env.memoize(bad);
ammAlice.withdraw(
bad,
1'000'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
seq(1),
ter(terNO_ACCOUNT));
// Invalid AMM
ammAlice.withdraw(
alice,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
{{USD, GBP}},
std::nullopt,
ter(terNO_AMM));
// Carol is not a Liquidity Provider
ammAlice.withdraw(
carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE));
// Withdrawing from one side.
// XRP by tokens
ammAlice.withdraw(
alice,
IOUAmount(9'999'999'9999, -4),
XRP(0),
std::nullopt,
ter(tecAMM_BALANCE));
// USD by tokens
ammAlice.withdraw(
alice,
IOUAmount(9'999'999'9, -1),
USD(0),
std::nullopt,
ter(tecAMM_BALANCE));
// XRP
ammAlice.withdraw(
alice,
XRP(10'000),
std::nullopt,
std::nullopt,
ter(tecAMM_BALANCE));
// USD
ammAlice.withdraw(
alice,
STAmount{USD, UINT64_C(9'999'9999999999999), -13},
std::nullopt,
std::nullopt,
ter(tecAMM_BALANCE));
});
testAMM(
[&](AMM& ammAlice, Env& env) {
// Withdraw entire one side of the pool.
// Pre-amendment:
// Equal withdraw but due to XRP rounding
// this results in full withdraw of XRP pool only,
// while leaving a tiny amount in USD pool.
// Post-amendment:
// Most of the pool is withdrawn with remaining tiny amounts
auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS)
: ter(tecAMM_BALANCE);
ammAlice.withdraw(
alice,
IOUAmount{9'999'999'9999, -4},
std::nullopt,
std::nullopt,
err);
if (env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
testAMM(
[&](AMM& ammAlice, Env& env) {
// Similar to above with even smaller remaining amount
// is it ok that the pool is unbalanced?
// Withdraw entire one side of the pool.
// Equal withdraw but due to XRP precision limit,
// this results in full withdraw of XRP pool only,
// while leaving a tiny amount in USD pool.
auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS)
: ter(tecAMM_BALANCE);
ammAlice.withdraw(
alice,
IOUAmount{9'999'999'999999999, -9},
std::nullopt,
std::nullopt,
err);
if (env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
// Invalid AMM
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.withdrawAll(alice);
ammAlice.withdraw(
alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
});
// Globally frozen asset
testAMM([&](AMM& ammAlice, Env& env) {
env(fset(gw, asfGlobalFreeze));
env.close();
// Can withdraw non-frozen token
ammAlice.withdraw(alice, XRP(100));
ammAlice.withdraw(
alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
ammAlice.withdraw(
alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
});
// Individually frozen (AMM) account
testAMM([&](AMM& ammAlice, Env& env) {
env(trust(gw, alice["USD"](0), tfSetFreeze));
env.close();
// Can withdraw non-frozen token
ammAlice.withdraw(alice, XRP(100));
ammAlice.withdraw(
alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
ammAlice.withdraw(
alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
env(trust(gw, alice["USD"](0), tfClearFreeze));
// Individually frozen AMM
env(trust(
gw,
STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
tfSetFreeze));
// Can withdraw non-frozen token
ammAlice.withdraw(alice, XRP(100));
ammAlice.withdraw(
alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
ammAlice.withdraw(
alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
});
// Carol withdraws more than she owns
testAMM([&](AMM& ammAlice, Env&) {
// Single deposit of 100000 worth of tokens,
// which is 10% of the pool. Carol is LP now.
ammAlice.deposit(carol, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
ammAlice.withdraw(
carol,
2'000'000,
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
});
// Withdraw with EPrice limit. Fails to withdraw, calculated tokens
// to withdraw are 0.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
auto const err = env.enabled(fixAMMv1_3)
? ter(tecAMM_INVALID_TOKENS)
: ter(tecAMM_FAILED);
ammAlice.withdraw(
carol, USD(100), std::nullopt, IOUAmount{500, 0}, err);
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
// Withdraw with EPrice limit. Fails to withdraw, calculated tokens
// to withdraw are greater than the LP shares.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdraw(
carol,
USD(100),
std::nullopt,
IOUAmount{600, 0},
ter(tecAMM_INVALID_TOKENS));
});
// Withdraw with EPrice limit. Fails to withdraw, amount1
// to withdraw is less than 1700USD.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdraw(
carol,
USD(1'700),
std::nullopt,
IOUAmount{520, 0},
ter(tecAMM_FAILED));
});
// Deposit/Withdraw the same amount with the trading fee
testAMM(
[&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(1'000));
ammAlice.withdraw(
carol,
USD(1'000),
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
},
std::nullopt,
1'000);
testAMM(
[&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, XRP(1'000));
ammAlice.withdraw(
carol,
XRP(1'000),
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
},
std::nullopt,
1'000);
// Deposit/Withdraw the same amount fails due to the tokens adjustment
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, STAmount{USD, 1, -6});
ammAlice.withdraw(
carol,
STAmount{USD, 1, -6},
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
});
// Withdraw close to one side of the pool. Account's LP tokens
// are rounded to all LP tokens.
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const err = env.enabled(fixAMMv1_3)
? ter(tecINVARIANT_FAILED)
: ter(tecAMM_BALANCE);
ammAlice.withdraw(
alice,
STAmount{USD, UINT64_C(9'999'999999999999), -12},
std::nullopt,
std::nullopt,
err);
},
{.features = {all, all - fixAMMv1_3}, .noLog = true});
// Tiny withdraw
testAMM([&](AMM& ammAlice, Env&) {
// XRP amount to withdraw is 0
ammAlice.withdraw(
alice,
IOUAmount{1, -5},
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Calculated tokens to withdraw are 0
ammAlice.withdraw(
alice,
std::nullopt,
STAmount{USD, 1, -11},
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
ammAlice.deposit(carol, STAmount{USD, 1, -10});
ammAlice.withdraw(
carol,
std::nullopt,
STAmount{USD, 1, -9},
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
ammAlice.withdraw(
carol,
std::nullopt,
XRPAmount{1},
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
ammAlice.withdraw(WithdrawArg{
.tokens = IOUAmount{1, -10},
.err = ter(tecAMM_INVALID_TOKENS)});
ammAlice.withdraw(WithdrawArg{
.asset1Out = STAmount{USD, 1, -15},
.asset2Out = XRPAmount{1},
.err = ter(tecAMM_INVALID_TOKENS)});
ammAlice.withdraw(WithdrawArg{
.tokens = IOUAmount{1, -10},
.asset1Out = STAmount{USD, 1, -15},
.err = ter(tecAMM_INVALID_TOKENS)});
});
}
void
testWithdraw()
{
testcase("Withdraw");
using namespace jtx;
auto const all = testable_amendments();
// Equal withdrawal by Carol: 1000000 of tokens, 10% of the current
// pool
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base.drops();
// Single deposit of 100000 worth of tokens,
// which is 10% of the pool. Carol is LP now.
ammAlice.deposit(carol, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
BEAST_EXPECT(
ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
// 30,000 less deposited 1,000
BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
// 30,000 less deposited 1,000 and 10 drops tx fee
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRPAmount{29'000'000'000 - baseFee}));
// Carol withdraws all tokens
ammAlice.withdraw(carol, 1'000'000);
BEAST_EXPECT(
ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero())));
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, carol, XRPAmount{30'000'000'000 - 2 * baseFee}));
});
// Equal withdrawal by tokens 1000000, 10%
// of the current pool
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, 1'000'000);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0}));
});
// Equal withdrawal with a limit. Withdraw XRP200.
// If proportional withdraw of USD is less than 100
// then withdraw that amount, otherwise withdraw USD100
// and proportionally withdraw XRP. It's the latter
// in this case - XRP100/USD100.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, XRP(200), USD(100));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
});
// Equal withdrawal with a limit. XRP100/USD100.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, XRP(100), USD(200));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
});
// Single withdrawal by amount XRP1000
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(alice, XRP(1'000));
if (!env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRP(9'000),
USD(10'000),
IOUAmount{9'486'832'98050514, -8}));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{9'000'000'001},
USD(10'000),
IOUAmount{9'486'832'98050514, -8}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
// Single withdrawal by tokens 10000.
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, 10'000, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0}));
});
// Withdraw all tokens.
testAMM([&](AMM& ammAlice, Env& env) {
env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000}));
// Can SetTrust only for AMM LP tokens
env(trust(
carol,
STAmount{
Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}),
ter(tecNO_PERMISSION));
env.close();
ammAlice.withdrawAll(alice);
BEAST_EXPECT(!ammAlice.ammExists());
BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));
// Can create AMM for the XRP/USD pair
AMM ammCarol(env, carol, XRP(10'000), USD(10'000));
BEAST_EXPECT(ammCarol.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
});
// Single deposit 1000USD, withdraw all tokens in USD
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, USD(1'000));
ammAlice.withdrawAll(carol, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
BEAST_EXPECT(
ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero())));
});
// Single deposit 1000USD, withdraw all tokens in XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, USD(1'000));
ammAlice.withdrawAll(carol, XRP(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(9'090'909'091),
STAmount{USD, UINT64_C(10'999'99999999999), -11},
IOUAmount{10'000'000, 0}));
});
// Single deposit/withdraw by the same account
testAMM(
[&](AMM& ammAlice, Env& env) {
// Since a smaller amount might be deposited due to
// the lp tokens adjustment, withdrawing by tokens
// is generally preferred to withdrawing by amount.
auto lpTokens = ammAlice.deposit(carol, USD(1'000));
ammAlice.withdraw(carol, lpTokens, USD(0));
lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6));
ammAlice.withdraw(carol, lpTokens, USD(0));
lpTokens = ammAlice.deposit(carol, XRPAmount(1));
ammAlice.withdraw(carol, lpTokens, XRPAmount(0));
if (!env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(10'000'000'001),
USD(10'000),
ammAlice.tokens()));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
// Single deposit by different accounts and then withdraw
// in reverse.
testAMM([&](AMM& ammAlice, Env&) {
auto const carolTokens = ammAlice.deposit(carol, USD(1'000));
auto const aliceTokens = ammAlice.deposit(alice, USD(1'000));
ammAlice.withdraw(alice, aliceTokens, USD(0));
ammAlice.withdraw(carol, carolTokens, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
BEAST_EXPECT(ammAlice.expectLPTokens(alice, ammAlice.tokens()));
});
// Equal deposit 10%, withdraw all tokens
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdrawAll(carol);
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
});
// Equal deposit 10%, withdraw all tokens in USD
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdrawAll(carol, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(9'090'909090909092), -12},
IOUAmount{10'000'000, 0}));
});
// Equal deposit 10%, withdraw all tokens in XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdrawAll(carol, XRP(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(9'090'909'091),
USD(11'000),
IOUAmount{10'000'000, 0}));
});
// Withdraw with EPrice limit.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdraw(
carol, USD(100), std::nullopt, IOUAmount{520, 0});
BEAST_EXPECT(ammAlice.expectLPTokens(
carol, IOUAmount{153'846'15384616, -8}));
if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(11'000'000'000),
STAmount{USD, UINT64_C(9'372'781065088757), -12},
IOUAmount{10'153'846'15384616, -8}));
else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(11'000'000'000),
STAmount{USD, UINT64_C(9'372'781065088769), -12},
IOUAmount{10'153'846'15384616, -8}));
else if (env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(11'000'000'000),
STAmount{USD, UINT64_C(9'372'78106508877), -11},
IOUAmount{10'153'846'15384616, -8}));
ammAlice.withdrawAll(carol);
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
},
{.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3},
.noLog = true});
// Withdraw with EPrice limit. AssetOut is 0.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
ammAlice.withdraw(
carol, USD(0), std::nullopt, IOUAmount{520, 0});
BEAST_EXPECT(ammAlice.expectLPTokens(
carol, IOUAmount{153'846'15384616, -8}));
if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(9'372'781065088757), -12},
IOUAmount{10'153'846'15384616, -8}));
else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(9'372'781065088769), -12},
IOUAmount{10'153'846'15384616, -8}));
else if (env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000),
STAmount{USD, UINT64_C(9'372'78106508877), -11},
IOUAmount{10'153'846'15384616, -8}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
// IOU to IOU + transfer fee
{
Env env{*this};
fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
env(rate(gw, 1.25));
env.close();
// no transfer fee on create
AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
BEAST_EXPECT(ammAlice.expectBalances(
USD(20'000), BTC(0.5), IOUAmount{100, 0}));
BEAST_EXPECT(expectHolding(env, alice, USD(0)));
BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
// no transfer fee on deposit
ammAlice.deposit(carol, 10);
BEAST_EXPECT(ammAlice.expectBalances(
USD(22'000), BTC(0.55), IOUAmount{110, 0}));
BEAST_EXPECT(expectHolding(env, carol, USD(0)));
BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
// no transfer fee on withdraw
ammAlice.withdraw(carol, 10);
BEAST_EXPECT(ammAlice.expectBalances(
USD(20'000), BTC(0.5), IOUAmount{100, 0}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0}));
BEAST_EXPECT(expectHolding(env, carol, USD(2'000)));
BEAST_EXPECT(expectHolding(env, carol, BTC(0.05)));
}
// Tiny withdraw
testAMM([&](AMM& ammAlice, Env&) {
// By tokens
ammAlice.withdraw(alice, IOUAmount{1, -3});
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{9'999'999'999},
STAmount{USD, UINT64_C(9'999'999999), -6},
IOUAmount{9'999'999'999, -3}));
});
testAMM(
[&](AMM& ammAlice, Env& env) {
// Single XRP pool
ammAlice.withdraw(alice, std::nullopt, XRPAmount{1});
if (!env.enabled(fixAMMv1_3))
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{9'999'999'999},
USD(10'000),
IOUAmount{9'999'999'9995, -4}));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
USD(10'000),
IOUAmount{9'999'999'9995, -4}));
},
std::nullopt,
0,
std::nullopt,
{all, all - fixAMMv1_3});
testAMM([&](AMM& ammAlice, Env&) {
// Single USD pool
ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(9'999'9999999999), -10},
IOUAmount{9'999'999'99999995, -8}));
});
// Withdraw close to entire pool
// Equal by tokens
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, IOUAmount{9'999'999'999, -3});
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3}));
});
// USD by tokens
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, IOUAmount{9'999'999}, USD(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1}));
});
// XRP by tokens
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, IOUAmount{9'999'900}, XRP(0));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{1}, USD(10'000), IOUAmount{100}));
});
// USD
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(
alice, STAmount{USD, UINT64_C(9'999'99999999999), -11});
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9}));
});
// XRP
testAMM([&](AMM& ammAlice, Env&) {
ammAlice.withdraw(alice, XRPAmount{9'999'999'999});
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{1}, USD(10'000), IOUAmount{100}));
});
}
void
testInvalidFeeVote()
{
testcase("Invalid Fee Vote");
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
ammAlice.vote(
std::nullopt,
1'000,
tfWithdrawAll,
std::nullopt,
std::nullopt,
ter(temINVALID_FLAG));
// Invalid fee.
ammAlice.vote(
std::nullopt,
1'001,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_FEE));
BEAST_EXPECT(ammAlice.expectTradingFee(0));
// Invalid Account
Account bad("bad");
env.memoize(bad);
ammAlice.vote(
bad,
1'000,
std::nullopt,
seq(1),
std::nullopt,
ter(terNO_ACCOUNT));
// Invalid AMM
ammAlice.vote(
alice,
1'000,
std::nullopt,
std::nullopt,
{{USD, GBP}},
ter(terNO_AMM));
// Account is not LP
ammAlice.vote(
carol,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
});
// Invalid AMM
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.withdrawAll(alice);
ammAlice.vote(
alice,
1'000,
std::nullopt,
std::nullopt,
std::nullopt,
ter(terNO_AMM));
});
}
void
testFeeVote()
{
testcase("Fee Vote");
using namespace jtx;
auto const all = testable_amendments();
// One vote sets fee to 1%.
testAMM([&](AMM& ammAlice, Env& env) {
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
ammAlice.vote({}, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
// Discounted fee is 1/10 of trading fee.
BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
});
auto vote = [&](AMM& ammAlice,
Env& env,
int i,
int fundUSD = 100'000,
std::uint32_t tokens = 10'000'000,
std::vector<Account>* accounts = nullptr) {
Account a(std::to_string(i));
// post-amendment the amount to deposit is slightly higher
// in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens
// fund just one USD higher in this case, which is enough for
// deposit to succeed
if (env.enabled(fixAMMv1_3))
++fundUSD;
fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct);
ammAlice.deposit(a, tokens);
ammAlice.vote(a, 50 * (i + 1));
if (accounts)
accounts->push_back(std::move(a));
};
// Eight votes fill all voting slots, set fee 0.175%.
testAMM(
[&](AMM& ammAlice, Env& env) {
for (int i = 0; i < 7; ++i)
vote(ammAlice, env, i, 10'000);
BEAST_EXPECT(ammAlice.expectTradingFee(175));
},
std::nullopt,
0,
std::nullopt,
{all});
// Eight votes fill all voting slots, set fee 0.175%.
// New vote, same account, sets fee 0.225%
testAMM([&](AMM& ammAlice, Env& env) {
for (int i = 0; i < 7; ++i)
vote(ammAlice, env, i);
BEAST_EXPECT(ammAlice.expectTradingFee(175));
Account const a("0");
ammAlice.vote(a, 450);
BEAST_EXPECT(ammAlice.expectTradingFee(225));
});
// Eight votes fill all voting slots, set fee 0.175%.
// New vote, new account, higher vote weight, set higher fee 0.244%
testAMM([&](AMM& ammAlice, Env& env) {
for (int i = 0; i < 7; ++i)
vote(ammAlice, env, i);
BEAST_EXPECT(ammAlice.expectTradingFee(175));
vote(ammAlice, env, 7, 100'000, 20'000'000);
BEAST_EXPECT(ammAlice.expectTradingFee(244));
});
// Eight votes fill all voting slots, set fee 0.219%.
// New vote, new account, higher vote weight, set smaller fee 0.206%
testAMM([&](AMM& ammAlice, Env& env) {
for (int i = 7; i > 0; --i)
vote(ammAlice, env, i);
BEAST_EXPECT(ammAlice.expectTradingFee(219));
vote(ammAlice, env, 0, 100'000, 20'000'000);
BEAST_EXPECT(ammAlice.expectTradingFee(206));
});
// Eight votes fill all voting slots. The accounts then withdraw all
// tokens. An account sets a new fee and the previous slots are
// deleted.
testAMM([&](AMM& ammAlice, Env& env) {
std::vector<Account> accounts;
for (int i = 0; i < 7; ++i)
vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
BEAST_EXPECT(ammAlice.expectTradingFee(175));
for (int i = 0; i < 7; ++i)
ammAlice.withdrawAll(accounts[i]);
ammAlice.deposit(carol, 10'000'000);
ammAlice.vote(carol, 1'000);
// The initial LP set the fee to 1000. Carol gets 50% voting
// power, and the new fee is 500.
BEAST_EXPECT(ammAlice.expectTradingFee(500));
});
// Eight votes fill all voting slots. The accounts then withdraw some
// tokens. The new vote doesn't get the voting power but
// the slots are refreshed and the fee is updated.
testAMM([&](AMM& ammAlice, Env& env) {
std::vector<Account> accounts;
for (int i = 0; i < 7; ++i)
vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
BEAST_EXPECT(ammAlice.expectTradingFee(175));
for (int i = 0; i < 7; ++i)
ammAlice.withdraw(accounts[i], 9'000'000);
ammAlice.deposit(carol, 1'000);
// The vote is not added to the slots
ammAlice.vote(carol, 1'000);
auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
for (std::uint16_t i = 0; i < info.size(); ++i)
BEAST_EXPECT(info[i][jss::account] != carol.human());
// But the slots are refreshed and the fee is changed
BEAST_EXPECT(ammAlice.expectTradingFee(82));
});
}
void
testInvalidBid()
{
testcase("Invalid Bid");
using namespace jtx;
using namespace std::chrono;
// burn all the LPTokens through a AMMBid transaction
{
Env env(*this);
fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
// auction slot is owned by the creator of the AMM i.e. gw
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
// gw attempts to burn all her LPTokens through a bid transaction
// this transaction fails because AMMBid transaction can not burn
// all the outstanding LPTokens
env(amm.bid({
.account = gw,
.bidMin = 1'000'000,
}),
ter(tecAMM_INVALID_TOKENS));
}
// burn all the LPTokens through a AMMBid transaction
{
Env env(*this);
fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
// auction slot is owned by the creator of the AMM i.e. gw
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
// gw burns all but one of its LPTokens through a bid transaction
// this transaction suceeds because the bid price is less than
// the total outstanding LPToken balance
env(amm.bid({
.account = gw,
.bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
}),
ter(tesSUCCESS))
.close();
// gw must own the auction slot
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
// 999'999 tokens are burned, only 1 LPToken is owned by gw
BEAST_EXPECT(
amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));
// gw owns only 1 LPToken in its balance
BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1);
// gw attempts to burn the last of its LPTokens in an AMMBid
// transaction. This transaction fails because it would burn all
// the remaining LPTokens
env(amm.bid({
.account = gw,
.bidMin = 1,
}),
ter(tecAMM_INVALID_TOKENS));
}
testAMM([&](AMM& ammAlice, Env& env) {
// Invalid flags
env(ammAlice.bid({
.account = carol,
.bidMin = 0,
.flags = tfWithdrawAll,
}),
ter(temINVALID_FLAG));
ammAlice.deposit(carol, 1'000'000);
// Invalid Bid price <= 0
for (auto bid : {0, -100})
{
env(ammAlice.bid({
.account = carol,
.bidMin = bid,
}),
ter(temBAD_AMOUNT));
env(ammAlice.bid({
.account = carol,
.bidMax = bid,
}),
ter(temBAD_AMOUNT));
}
// Invlaid Min/Max combination
env(ammAlice.bid({
.account = carol,
.bidMin = 200,
.bidMax = 100,
}),
ter(tecAMM_INVALID_TOKENS));
// Invalid Account
Account bad("bad");
env.memoize(bad);
env(ammAlice.bid({
.account = bad,
.bidMax = 100,
}),
seq(1),
ter(terNO_ACCOUNT));
// Account is not LP
Account const dan("dan");
env.fund(XRP(1'000), dan);
env(ammAlice.bid({
.account = dan,
.bidMin = 100,
}),
ter(tecAMM_INVALID_TOKENS));
env(ammAlice.bid({
.account = dan,
}),
ter(tecAMM_INVALID_TOKENS));
// Auth account is invalid.
env(ammAlice.bid({
.account = carol,
.bidMin = 100,
.authAccounts = {bob},
}),
ter(terNO_ACCOUNT));
// Invalid Assets
env(ammAlice.bid({
.account = alice,
.bidMax = 100,
.assets = {{USD, GBP}},
}),
ter(terNO_AMM));
// Invalid Min/Max issue
env(ammAlice.bid({
.account = alice,
.bidMax = STAmount{USD, 100},
}),
ter(temBAD_AMM_TOKENS));
env(ammAlice.bid({
.account = alice,
.bidMin = STAmount{USD, 100},
}),
ter(temBAD_AMM_TOKENS));
});
// Invalid AMM
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.withdrawAll(alice);
env(ammAlice.bid({
.account = alice,
.bidMax = 100,
}),
ter(terNO_AMM));
});
// More than four Auth accounts.
testAMM([&](AMM& ammAlice, Env& env) {
Account ed("ed");
Account bill("bill");
Account scott("scott");
Account james("james");
env.fund(XRP(1'000), bob, ed, bill, scott, james);
env.close();
ammAlice.deposit(carol, 1'000'000);
env(ammAlice.bid({
.account = carol,
.bidMin = 100,
.authAccounts = {bob, ed, bill, scott, james},
}),
ter(temMALFORMED));
});
// Bid price exceeds LP owned tokens
testAMM([&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct);
ammAlice.deposit(carol, 1'000'000);
ammAlice.deposit(bob, 10);
env(ammAlice.bid({
.account = carol,
.bidMin = 1'000'001,
}),
ter(tecAMM_INVALID_TOKENS));
env(ammAlice.bid({
.account = carol,
.bidMax = 1'000'001,
}),
ter(tecAMM_INVALID_TOKENS));
env(ammAlice.bid({
.account = carol,
.bidMin = 1'000,
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
// Slot purchase price is more than 1000 but bob only has 10 tokens
env(ammAlice.bid({
.account = bob,
}),
ter(tecAMM_INVALID_TOKENS));
});
// Bid all tokens, still own the slot
{
Env env(*this);
fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'000)});
AMM amm(env, gw, XRP(10), USD(1'000));
auto const lpIssue = amm.lptIssue();
env.trust(STAmount{lpIssue, 100}, alice);
env.trust(STAmount{lpIssue, 50}, bob);
env(pay(gw, alice, STAmount{lpIssue, 100}));
env(pay(gw, bob, STAmount{lpIssue, 50}));
env(amm.bid({.account = alice, .bidMin = 100}));
// Alice doesn't have any more tokens, but
// she still owns the slot.
env(amm.bid({
.account = bob,
.bidMax = 50,
}),
ter(tecAMM_FAILED));
}
}
void
testBid(FeatureBitset features)
{
testcase("Bid");
using namespace jtx;
using namespace std::chrono;
// Auction slot initially is owned by AMM creator, who pays 0 price.
// Bid 110 tokens. Pay bidMin.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
env(ammAlice.bid({.account = carol, .bidMin = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
// 110 tokens are burned.
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Bid with min/max when the pay price is less than min.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
// Bid exactly 110. Pay 110 because the pay price is < 110.
env(ammAlice.bid(
{.account = carol, .bidMin = 110, .bidMax = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
// Bid exactly 180-200. Pay 180 because the pay price is < 180.
env(ammAlice.bid(
{.account = alice, .bidMin = 180, .bidMax = 200}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
BEAST_EXPECT(ammAlice.expectBalances(
XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Start bid at bidMin 110.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
// Bid, pay bidMin.
env(ammAlice.bid({.account = carol, .bidMin = 110}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
ammAlice.deposit(bob, 1'000'000);
// Bid, pay the computed price.
env(ammAlice.bid({.account = bob}));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
// Bid bidMax fails because the computed price is higher.
env(ammAlice.bid({
.account = carol,
.bidMax = 120,
}),
ter(tecAMM_FAILED));
// Bid MaxSlotPrice succeeds - pay computed price
env(ammAlice.bid({.account = carol, .bidMax = 600}));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
// Bid Min/MaxSlotPrice fails because the computed price is not
// in range
env(ammAlice.bid({
.account = carol,
.bidMin = 10,
.bidMax = 100,
}),
ter(tecAMM_FAILED));
// Bid Min/MaxSlotPrice succeeds - pay computed price
env(ammAlice.bid(
{.account = carol, .bidMin = 100, .bidMax = 600}));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Slot states.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
ammAlice.deposit(bob, 1'000'000);
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{12'000'000'001},
USD(12'000),
IOUAmount{12'000'000, 0}));
// Initial state. Pay bidMin.
env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
// 1st Interval after close, price for 0th interval.
env(ammAlice.bid({.account = bob}));
env.close(seconds(AUCTION_SLOT_INTERVAL_DURATION + 1));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
// 10th Interval after close, price for 1st interval.
env(ammAlice.bid({.account = carol}));
env.close(seconds(10 * AUCTION_SLOT_INTERVAL_DURATION + 1));
BEAST_EXPECT(
ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
// 20th Interval (expired) after close, price for 10th interval.
env(ammAlice.bid({.account = bob}));
env.close(seconds(
AUCTION_SLOT_TIME_INTERVALS *
AUCTION_SLOT_INTERVAL_DURATION +
1));
BEAST_EXPECT(ammAlice.expectAuctionSlot(
0, std::nullopt, IOUAmount{127'33875, -5}));
// 0 Interval.
env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
BEAST_EXPECT(ammAlice.expectAuctionSlot(
0, std::nullopt, IOUAmount{110}));
// ~321.09 tokens burnt on bidding fees.
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(12'000),
USD(12'000),
IOUAmount{11'999'678'91, -2}));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{12'000'000'001},
USD(12'000),
IOUAmount{11'999'678'91, -2}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Pool's fee 1%. Bid bidMin.
// Auction slot owner and auth account trade at discounted fee -
// 1/10 of the trading fee.
// Other accounts trade at 1% fee.
testAMM(
[&](AMM& ammAlice, Env& env) {
Account const dan("dan");
Account const ed("ed");
fund(env, gw, {bob, dan, ed}, {USD(20'000)}, Fund::Acct);
ammAlice.deposit(bob, 1'000'000);
ammAlice.deposit(ed, 1'000'000);
ammAlice.deposit(carol, 500'000);
ammAlice.deposit(dan, 500'000);
auto ammTokens = ammAlice.getLPTokensBalance();
env(ammAlice.bid({
.account = carol,
.bidMin = 120,
.authAccounts = {bob, ed},
}));
auto const slotPrice = IOUAmount{5'200};
ammTokens -= slotPrice;
BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000), USD(13'000), ammTokens));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'003}, USD(13'000), ammTokens));
// Discounted trade
for (int i = 0; i < 10; ++i)
{
auto tokens = ammAlice.deposit(carol, USD(100));
ammAlice.withdraw(carol, tokens, USD(0));
tokens = ammAlice.deposit(bob, USD(100));
ammAlice.withdraw(bob, tokens, USD(0));
tokens = ammAlice.deposit(ed, USD(100));
ammAlice.withdraw(ed, tokens, USD(0));
}
// carol, bob, and ed pay ~0.99USD in fees.
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'499'00572620545), -11));
BEAST_EXPECT(
env.balance(bob, USD) ==
STAmount(USD, UINT64_C(18'999'00572616195), -11));
BEAST_EXPECT(
env.balance(ed, USD) ==
STAmount(USD, UINT64_C(18'999'00572611841), -11));
// USD pool is slightly higher because of the fees.
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount(USD, UINT64_C(13'002'98282151419), -11),
ammTokens));
}
else
{
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'499'00572620544), -11));
BEAST_EXPECT(
env.balance(bob, USD) ==
STAmount(USD, UINT64_C(18'999'00572616194), -11));
BEAST_EXPECT(
env.balance(ed, USD) ==
STAmount(USD, UINT64_C(18'999'0057261184), -10));
// USD pool is slightly higher because of the fees.
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount(USD, UINT64_C(13'002'98282151422), -11),
ammTokens));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'003},
STAmount(USD, UINT64_C(13'002'98282151422), -11),
ammTokens));
}
ammTokens = ammAlice.getLPTokensBalance();
// Trade with the fee
for (int i = 0; i < 10; ++i)
{
auto const tokens = ammAlice.deposit(dan, USD(100));
ammAlice.withdraw(dan, tokens, USD(0));
}
// dan pays ~9.94USD, which is ~10 times more in fees than
// carol, bob, ed. the discounted fee is 10 times less
// than the trading fee.
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(
env.balance(dan, USD) ==
STAmount(USD, UINT64_C(19'490'056722744), -9));
// USD pool gains more in dan's fees.
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount{USD, UINT64_C(13'012'92609877019), -11},
ammTokens));
// Discounted fee payment
ammAlice.deposit(carol, USD(100));
ammTokens = ammAlice.getLPTokensBalance();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount{USD, UINT64_C(13'112'92609877019), -11},
ammTokens));
env(pay(carol, bob, USD(100)),
path(~USD),
sendmax(XRP(110)));
env.close();
// carol pays 100000 drops in fees
// 99900668XRP swapped in for 100USD
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'100'000'668},
STAmount{USD, UINT64_C(13'012'92609877019), -11},
ammTokens));
}
else
{
if (!features[fixAMMv1_3])
BEAST_EXPECT(
env.balance(dan, USD) ==
STAmount(USD, UINT64_C(19'490'05672274399), -11));
else
BEAST_EXPECT(
env.balance(dan, USD) ==
STAmount(USD, UINT64_C(19'490'05672274398), -11));
// USD pool gains more in dan's fees.
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount{USD, UINT64_C(13'012'92609877023), -11},
ammTokens));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'003},
STAmount{USD, UINT64_C(13'012'92609877024), -11},
ammTokens));
// Discounted fee payment
ammAlice.deposit(carol, USD(100));
ammTokens = ammAlice.getLPTokensBalance();
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(13'000),
STAmount{USD, UINT64_C(13'112'92609877023), -11},
ammTokens));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'003},
STAmount{USD, UINT64_C(13'112'92609877024), -11},
ammTokens));
env(pay(carol, bob, USD(100)),
path(~USD),
sendmax(XRP(110)));
env.close();
// carol pays 100000 drops in fees
// 99900668XRP swapped in for 100USD
if (!features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'100'000'668},
STAmount{USD, UINT64_C(13'012'92609877023), -11},
ammTokens));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'100'000'671},
STAmount{USD, UINT64_C(13'012'92609877024), -11},
ammTokens));
}
// Payment with the trading fee
env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110)));
env.close();
// alice pays ~1.011USD in fees, which is ~10 times more
// than carol's fee
// 100.099431529USD swapped in for 100XRP
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'668},
STAmount{USD, UINT64_C(13'114'03663047264), -11},
ammTokens));
}
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'668},
STAmount{USD, UINT64_C(13'114'03663047269), -11},
ammTokens));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'671},
STAmount{USD, UINT64_C(13'114'03663044937), -11},
ammTokens));
}
// Auction slot expired, no discounted fee
env.close(seconds(TOTAL_TIME_SLOT_SECS + 1));
// clock is parent's based
env.close();
if (!features[fixAMMv1_1])
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'399'00572620545), -11));
else if (!features[fixAMMv1_3])
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'399'00572620544), -11));
ammTokens = ammAlice.getLPTokensBalance();
for (int i = 0; i < 10; ++i)
{
auto const tokens = ammAlice.deposit(carol, USD(100));
ammAlice.withdraw(carol, tokens, USD(0));
}
// carol pays ~9.94USD in fees, which is ~10 times more in
// trading fees vs discounted fee.
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'389'06197177128), -11));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'668},
STAmount{USD, UINT64_C(13'123'98038490681), -11},
ammTokens));
}
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'389'06197177124), -11));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'668},
STAmount{USD, UINT64_C(13'123'98038490689), -11},
ammTokens));
}
else
{
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'389'06197177129), -11));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{13'000'000'671},
STAmount{USD, UINT64_C(13'123'98038488352), -11},
ammTokens));
}
env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
env.close();
// carol pays ~1.008XRP in trading fee, which is
// ~10 times more than the discounted fee.
// 99.815876XRP is swapped in for 100USD
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(13'100'824'790),
STAmount{USD, UINT64_C(13'023'98038490681), -11},
ammTokens));
}
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(13'100'824'790),
STAmount{USD, UINT64_C(13'023'98038490689), -11},
ammTokens));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(13'100'824'793),
STAmount{USD, UINT64_C(13'023'98038488352), -11},
ammTokens));
}
},
std::nullopt,
1'000,
std::nullopt,
{features});
// Bid tiny amount
testAMM(
[&](AMM& ammAlice, Env& env) {
// Bid a tiny amount
auto const tiny =
Number{STAmount::cMinValue, STAmount::cMinOffset};
env(ammAlice.bid(
{.account = alice, .bidMin = IOUAmount{tiny}}));
// Auction slot purchase price is equal to the tiny amount
// since the minSlotPrice is 0 with no trading fee.
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
// The purchase price is too small to affect the total tokens
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
// Bid the tiny amount
env(ammAlice.bid({
.account = alice,
.bidMin =
IOUAmount{STAmount::cMinValue, STAmount::cMinOffset},
}));
// Pay slightly higher price
BEAST_EXPECT(ammAlice.expectAuctionSlot(
0, 0, IOUAmount{tiny * Number{105, -2}}));
// The purchase price is still too small to affect the total
// tokens
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), ammAlice.tokens()));
},
std::nullopt,
0,
std::nullopt,
{features});
// Reset auth account
testAMM(
[&](AMM& ammAlice, Env& env) {
env(ammAlice.bid({
.account = alice,
.bidMin = IOUAmount{100},
.authAccounts = {carol},
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({carol}));
env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
Account bob("bob");
Account dan("dan");
fund(env, {bob, dan}, XRP(1'000));
env(ammAlice.bid({
.account = alice,
.bidMin = IOUAmount{100},
.authAccounts = {bob, dan},
}));
BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Bid all tokens, still own the slot and trade at a discount
{
Env env(*this, features);
fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
auto const lpIssue = amm.lptIssue();
env.trust(STAmount{lpIssue, 500}, alice);
env.trust(STAmount{lpIssue, 50}, bob);
env(pay(gw, alice, STAmount{lpIssue, 500}));
env(pay(gw, bob, STAmount{lpIssue, 50}));
// Alice doesn't have anymore lp tokens
env(amm.bid({.account = alice, .bidMin = 500}));
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0}));
// But trades with the discounted fee since she still owns the slot.
// Alice pays 10011 drops in fees
env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11)));
BEAST_EXPECT(amm.expectBalances(
XRPAmount{1'010'010'011},
USD(1'000),
IOUAmount{1'004'487'562112089, -9}));
// Bob pays the full fee ~0.1USD
env(pay(bob, alice, XRP(10)), path(~XRP), sendmax(USD(11)));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount{1'000'010'011},
STAmount{USD, UINT64_C(1'010'10090898081), -11},
IOUAmount{1'004'487'562112089, -9}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount{1'000'010'011},
STAmount{USD, UINT64_C(1'010'100908980811), -12},
IOUAmount{1'004'487'562112089, -9}));
}
}
// preflight tests
{
Env env(*this, features);
auto const baseFee = env.current()->fees().base;
fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
Json::Value tx = amm.bid({.account = alice, .bidMin = 500});
{
auto jtx = env.jt(tx, seq(1), fee(baseFee));
env.app().config().features.erase(featureAMM);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = Transactor::invokePreflight<AMMBid>(pfctx);
BEAST_EXPECT(pf == temDISABLED);
env.app().config().features.insert(featureAMM);
}
{
auto jtx = env.jt(tx, seq(1), fee(baseFee));
jtx.jv["TxnSignature"] = "deadbeef";
jtx.stx = env.ust(jtx);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = Transactor::invokePreflight<AMMBid>(pfctx);
BEAST_EXPECT(pf != tesSUCCESS);
}
{
auto jtx = env.jt(tx, seq(1), fee(baseFee));
jtx.jv["Asset2"]["currency"] = "XRP";
jtx.jv["Asset2"].removeMember("issuer");
jtx.stx = env.ust(jtx);
PreflightContext pfctx(
env.app(),
*jtx.stx,
env.current()->rules(),
tapNONE,
env.journal);
auto pf = Transactor::invokePreflight<AMMBid>(pfctx);
BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
}
}
}
void
testInvalidAMMPayment()
{
testcase("Invalid AMM Payment");
using namespace jtx;
using namespace std::chrono;
using namespace std::literals::chrono_literals;
// Can't pay into AMM account.
// Can't pay out since there is no keys
for (auto const& acct : {gw, alice})
{
{
Env env(*this);
fund(env, gw, {alice, carol}, XRP(1'000), {USD(100)});
// XRP balance is below reserve
AMM ammAlice(env, acct, XRP(10), USD(10));
// Pay below reserve
env(pay(carol, ammAlice.ammAccount(), XRP(10)),
ter(tecNO_PERMISSION));
// Pay above reserve
env(pay(carol, ammAlice.ammAccount(), XRP(300)),
ter(tecNO_PERMISSION));
// Pay IOU
env(pay(carol, ammAlice.ammAccount(), USD(10)),
ter(tecNO_PERMISSION));
}
{
Env env(*this);
fund(env, gw, {alice, carol}, XRP(10'000'000), {USD(10'000)});
// XRP balance is above reserve
AMM ammAlice(env, acct, XRP(1'000'000), USD(100));
// Pay below reserve
env(pay(carol, ammAlice.ammAccount(), XRP(10)),
ter(tecNO_PERMISSION));
// Pay above reserve
env(pay(carol, ammAlice.ammAccount(), XRP(1'000'000)),
ter(tecNO_PERMISSION));
}
}
// Can't pay into AMM with escrow.
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base;
env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
escrow::cancel_time(env.now() + 2s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
});
// Can't pay into AMM with paychan.
testAMM([&](AMM& ammAlice, Env& env) {
auto const pk = carol.pk();
auto const settleDelay = 100s;
NetClock::time_point const cancelAfter =
env.current()->info().parentCloseTime + 200s;
env(paychan::create(
carol,
ammAlice.ammAccount(),
XRP(1'000),
settleDelay,
pk,
cancelAfter),
ter(tecNO_PERMISSION));
});
// Can't pay into AMM with checks.
testAMM([&](AMM& ammAlice, Env& env) {
env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)),
ter(tecNO_PERMISSION));
});
// Pay amounts close to one side of the pool
testAMM(
[&](AMM& ammAlice, Env& env) {
// Can't consume whole pool
env(pay(alice, carol, USD(100)),
path(~USD),
sendmax(XRP(1'000'000'000)),
ter(tecPATH_PARTIAL));
env(pay(alice, carol, XRP(100)),
path(~XRP),
sendmax(USD(1'000'000'000)),
ter(tecPATH_PARTIAL));
// Overflow
env(pay(alice,
carol,
STAmount{USD, UINT64_C(99'999999999), -9}),
path(~USD),
sendmax(XRP(1'000'000'000)),
ter(tecPATH_PARTIAL));
env(pay(alice,
carol,
STAmount{USD, UINT64_C(999'99999999), -8}),
path(~USD),
sendmax(XRP(1'000'000'000)),
ter(tecPATH_PARTIAL));
env(pay(alice, carol, STAmount{xrpIssue(), 99'999'999}),
path(~XRP),
sendmax(USD(1'000'000'000)),
ter(tecPATH_PARTIAL));
// Sender doesn't have enough funds
env(pay(alice, carol, USD(99.99)),
path(~USD),
sendmax(XRP(1'000'000'000)),
ter(tecPATH_PARTIAL));
env(pay(alice, carol, STAmount{xrpIssue(), 99'990'000}),
path(~XRP),
sendmax(USD(1'000'000'000)),
ter(tecPATH_PARTIAL));
},
{{XRP(100), USD(100)}});
// Globally frozen
testAMM([&](AMM& ammAlice, Env& env) {
env(fset(gw, asfGlobalFreeze));
env.close();
env(pay(alice, carol, USD(1)),
path(~USD),
txflags(tfPartialPayment | tfNoRippleDirect),
sendmax(XRP(10)),
ter(tecPATH_DRY));
env(pay(alice, carol, XRP(1)),
path(~XRP),
txflags(tfPartialPayment | tfNoRippleDirect),
sendmax(USD(10)),
ter(tecPATH_DRY));
});
// Individually frozen AMM
testAMM([&](AMM& ammAlice, Env& env) {
env(trust(
gw,
STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
tfSetFreeze));
env.close();
env(pay(alice, carol, USD(1)),
path(~USD),
txflags(tfPartialPayment | tfNoRippleDirect),
sendmax(XRP(10)),
ter(tecPATH_DRY));
env(pay(alice, carol, XRP(1)),
path(~XRP),
txflags(tfPartialPayment | tfNoRippleDirect),
sendmax(USD(10)),
ter(tecPATH_DRY));
});
// Individually frozen accounts
testAMM([&](AMM& ammAlice, Env& env) {
env(trust(gw, carol["USD"](0), tfSetFreeze));
env(trust(gw, alice["USD"](0), tfSetFreeze));
env.close();
env(pay(alice, carol, XRP(1)),
path(~XRP),
sendmax(USD(10)),
txflags(tfNoRippleDirect | tfPartialPayment),
ter(tecPATH_DRY));
});
}
void
testBasicPaymentEngine(FeatureBitset features)
{
testcase("Basic Payment");
using namespace jtx;
// Payment 100USD for 100XRP.
// Force one path with tfNoRippleDirect.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(jtx::XRP(30'000), bob);
env.close();
env(pay(bob, carol, USD(100)),
path(~USD),
sendmax(XRP(100)),
txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// Initial balance 30,000 + 100
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
// Initial balance 30,000 - 100(sendmax) - 10(tx fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// Payment 100USD for 100XRP, use default path.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(jtx::XRP(30'000), bob);
env.close();
env(pay(bob, carol, USD(100)), sendmax(XRP(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// Initial balance 30,000 + 100
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
// Initial balance 30,000 - 100(sendmax) - 10(tx fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// This payment is identical to above. While it has
// both default path and path, activeStrands has one path.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(jtx::XRP(30'000), bob);
env.close();
env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// Initial balance 30,000 + 100
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
// Initial balance 30,000 - 100(sendmax) - 10(tx fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// Payment with limitQuality set.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(jtx::XRP(30'000), bob);
env.close();
// Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
// would have been sent has it not been for limitQuality.
env(pay(bob, carol, USD(100)),
path(~USD),
sendmax(XRP(100)),
txflags(
tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'010), USD(10'000), ammAlice.tokens()));
// Initial balance 30,000 + 10(limited by limitQuality)
BEAST_EXPECT(expectHolding(env, carol, USD(30'010)));
// Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
// fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
// Fails because of limitQuality. Would have sent
// ~98.91USD/110XRP has it not been for limitQuality.
env(pay(bob, carol, USD(100)),
path(~USD),
sendmax(XRP(100)),
txflags(
tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
ter(tecPATH_DRY));
env.close();
},
{{XRP(10'000), USD(10'010)}},
0,
std::nullopt,
{features});
// Payment with limitQuality and transfer fee set.
testAMM(
[&](AMM& ammAlice, Env& env) {
env(rate(gw, 1.1));
env.close();
env.fund(jtx::XRP(30'000), bob);
env.close();
// Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
// would have been sent has it not been for limitQuality and
// the transfer fee.
env(pay(bob, carol, USD(100)),
path(~USD),
sendmax(XRP(110)),
txflags(
tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'010), USD(10'000), ammAlice.tokens()));
// 10USD - 10% transfer fee
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'009'09090909091), -11}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
},
{{XRP(10'000), USD(10'010)}},
0,
std::nullopt,
{features});
// Fail when partial payment is not set.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(jtx::XRP(30'000), bob);
env.close();
env(pay(bob, carol, USD(100)),
path(~USD),
sendmax(XRP(100)),
txflags(tfNoRippleDirect),
ter(tecPATH_PARTIAL));
},
{{XRP(10'000), USD(10'000)}},
0,
std::nullopt,
{features});
// Non-default path (with AMM) has a better quality than default path.
// The max possible liquidity is taken out of non-default
// path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest
// is taken from the offer.
{
Env env(*this, features);
fund(
env, gw, {alice, carol}, {USD(30'000), EUR(30'000)}, Fund::All);
env.close();
env.fund(XRP(1'000), bob);
env.close();
auto ammEUR_XRP = AMM(env, alice, XRP(10'000), EUR(10'000));
auto ammUSD_EUR = AMM(env, alice, EUR(10'000), USD(10'000));
env(offer(alice, XRP(101), USD(100)), txflags(tfPassive));
env.close();
env(pay(bob, carol, USD(100)),
path(~EUR, ~USD),
sendmax(XRP(102)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammEUR_XRP.expectBalances(
XRPAmount(10'030'082'730),
STAmount(EUR, UINT64_C(9'970'007498125468), -12),
ammEUR_XRP.tokens()));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammUSD_EUR.expectBalances(
STAmount(USD, UINT64_C(9'970'097277662122), -12),
STAmount(EUR, UINT64_C(10'029'99250187452), -11),
ammUSD_EUR.tokens()));
// fixReducedOffersV2 changes the expected results slightly.
Amounts const expectedAmounts =
env.closed()->rules().enabled(fixReducedOffersV2)
? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)}
: Amounts{
XRPAmount(30'201'749),
STAmount(USD, UINT64_C(29'90272233787818), -14)};
BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
}
else
{
BEAST_EXPECT(ammUSD_EUR.expectBalances(
STAmount(USD, UINT64_C(9'970'097277662172), -12),
STAmount(EUR, UINT64_C(10'029'99250187452), -11),
ammUSD_EUR.tokens()));
// fixReducedOffersV2 changes the expected results slightly.
Amounts const expectedAmounts =
env.closed()->rules().enabled(fixReducedOffersV2)
? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)}
: Amounts{
XRPAmount(30'201'749),
STAmount(USD, UINT64_C(29'90272233782840), -14)};
BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
}
// Initial 30,000 + 100
BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100}));
// Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env,
bob,
XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} -
txfee(env, 1)));
}
// Default path (with AMM) has a better quality than a non-default path.
// The max possible liquidity is taken out of default
// path ~49XRP/49USD. The rest is taken from the offer.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
env.close();
env.trust(EUR(2'000), alice);
env.close();
env(pay(gw, alice, EUR(1'000)));
env(offer(alice, XRP(101), EUR(100)), txflags(tfPassive));
env.close();
env(offer(alice, EUR(100), USD(100)), txflags(tfPassive));
env.close();
env(pay(bob, carol, USD(100)),
path(~EUR, ~USD),
sendmax(XRP(102)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(10'050'238'637),
STAmount(USD, UINT64_C(9'950'01249687578), -11),
ammAlice.tokens()));
BEAST_EXPECT(expectOffers(
env,
alice,
2,
{{Amounts{
XRPAmount(50'487'378),
STAmount(EUR, UINT64_C(49'98750312422), -11)},
Amounts{
STAmount(EUR, UINT64_C(49'98750312422), -11),
STAmount(USD, UINT64_C(49'98750312422), -11)}}}));
// Initial 30,000 + 99.99999999999
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'099'99999999999), -11}));
// Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
// fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env,
bob,
XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} -
txfee(env, 1)));
},
std::nullopt,
0,
std::nullopt,
{features});
// Default path with AMM and Order Book offer. AMM is consumed first,
// remaining amount is consumed by the offer.
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, {USD(100)}, Fund::Acct);
env.close();
env(offer(bob, XRP(100), USD(100)), txflags(tfPassive));
env.close();
env(pay(alice, carol, USD(200)),
sendmax(XRP(200)),
txflags(tfPartialPayment));
env.close();
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// Initial 30,000 + 200
BEAST_EXPECT(expectHolding(env, carol, USD(30'200)));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100),
STAmount(USD, UINT64_C(10'000'00000000001), -11),
ammAlice.tokens()));
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount(USD, UINT64_C(30'199'99999999999), -11)));
}
// Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
// - 100(offer) - 10(tx fee) - one reserve
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) -
ammCrtFee(env) - txfee(env, 1)));
BEAST_EXPECT(expectOffers(env, bob, 0));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// Default path with AMM and Order Book offer.
// Order Book offer is consumed first.
// Remaining amount is consumed by AMM.
{
Env env(*this, features);
fund(env, gw, {alice, bob, carol}, XRP(20'000), {USD(2'000)});
env.close();
env(offer(bob, XRP(50), USD(150)), txflags(tfPassive));
env.close();
AMM ammAlice(env, alice, XRP(1'000), USD(1'050));
env(pay(alice, carol, USD(200)),
sendmax(XRP(200)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(1'050), USD(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(2'200)));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
// Offer crossing XRP/IOU
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
env.close();
env(offer(bob, USD(100), XRP(100)));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'100), USD(10'000), ammAlice.tokens()));
// Initial 1,000 + 100
BEAST_EXPECT(expectHolding(env, bob, USD(1'100)));
// Initial 30,000 - 100(offer) - 10(tx fee)
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
BEAST_EXPECT(expectOffers(env, bob, 0));
},
{{XRP(10'000), USD(10'100)}},
0,
std::nullopt,
{features});
// Offer crossing IOU/IOU and transfer rate
// Single path AMM offer
testAMM(
[&](AMM& ammAlice, Env& env) {
env(rate(gw, 1.25));
env.close();
// This offer succeeds to cross pre- and post-amendment
// because the strand's out amount is small enough to match
// limitQuality value and limitOut() function in StrandFlow
// doesn't require an adjustment to out value.
env(offer(carol, EUR(100), GBP(100)));
env.close();
// No transfer fee
BEAST_EXPECT(ammAlice.expectBalances(
GBP(1'100), EUR(1'000), ammAlice.tokens()));
// Initial 30,000 - 100(offer) - 25% transfer fee
BEAST_EXPECT(expectHolding(env, carol, GBP(29'875)));
// Initial 30,000 + 100(offer)
BEAST_EXPECT(expectHolding(env, carol, EUR(30'100)));
BEAST_EXPECT(expectOffers(env, bob, 0));
},
{{GBP(1'000), EUR(1'100)}},
0,
std::nullopt,
{features});
// Single-path AMM offer
testAMM(
[&](AMM& amm, Env& env) {
env(rate(gw, 1.001));
env.close();
env(offer(carol, XRP(100), USD(55)));
env.close();
if (!features[fixAMMv1_1])
{
// Pre-amendment the transfer fee is not taken into
// account when calculating the limit out based on
// limitQuality. Carol pays 0.1% on the takerGets, which
// lowers the overall quality. AMM offer is generated based
// on higher limit out, which generates a larger offer
// with lower quality. Consequently, the offer fails
// to cross.
BEAST_EXPECT(
amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
BEAST_EXPECT(expectOffers(
env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else
{
// Post-amendment the transfer fee is taken into account
// when calculating the limit out based on limitQuality.
// This increases the limitQuality and decreases
// the limit out. Consequently, AMM offer size is decreased,
// and the quality is increased, matching the overall
// quality.
// AMM offer ~50USD/91XRP
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
STAmount{USD, UINT64_C(550'000000055), -9},
amm.tokens()));
// Offer ~91XRP/49.99USD
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{{Amounts{
XRPAmount{9'090'909},
STAmount{USD, 4'99999995, -8}}}}));
// Carol pays 0.1% fee on ~50USD =~ 0.05USD
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(29'949'94999999494), -11));
}
},
{{XRP(1'000), USD(500)}},
0,
std::nullopt,
{features});
testAMM(
[&](AMM& amm, Env& env) {
env(rate(gw, 1.001));
env.close();
env(offer(carol, XRP(10), USD(5.5)));
env.close();
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(amm.expectBalances(
XRP(990),
STAmount{USD, UINT64_C(505'050505050505), -12},
amm.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRP(990),
STAmount{USD, UINT64_C(505'0505050505051), -13},
amm.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
}
},
{{XRP(1'000), USD(500)}},
0,
std::nullopt,
{features});
// Multi-path AMM offer
testAMM(
[&](AMM& ammAlice, Env& env) {
Account const ed("ed");
fund(
env,
gw,
{bob, ed},
XRP(30'000),
{GBP(2'000), EUR(2'000)},
Fund::Acct);
env(rate(gw, 1.25));
env.close();
// The auto-bridge is worse quality than AMM, is not consumed
// first and initially forces multi-path AMM offer generation.
// Multi-path AMM offers are consumed until their quality
// is less than the auto-bridge offers quality. Auto-bridge
// offers are consumed afterward. Then the behavior is
// different pre-amendment and post-amendment.
env(offer(bob, GBP(10), XRP(10)), txflags(tfPassive));
env(offer(ed, XRP(10), EUR(10)), txflags(tfPassive));
env.close();
env(offer(carol, EUR(100), GBP(100)));
env.close();
if (!features[fixAMMv1_1])
{
// After the auto-bridge offers are consumed, single path
// AMM offer is generated with the limit out not taking
// into consideration the transfer fee. This results
// in an overall lower quality offer than the limit quality
// and the single path AMM offer fails to consume.
// Total consumed ~37.06GBP/39.32EUR
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{GBP, UINT64_C(1'037'06583722133), -11},
STAmount{EUR, UINT64_C(1'060'684828792831), -12},
ammAlice.tokens()));
// Consumed offer ~49.32EUR/49.32GBP
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{Amounts{
STAmount{EUR, UINT64_C(50'684828792831), -12},
STAmount{GBP, UINT64_C(50'684828792831), -12}}}));
BEAST_EXPECT(expectOffers(env, bob, 0));
BEAST_EXPECT(expectOffers(env, ed, 0));
// Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB))
// * 1.25
// = 58.825 = ~29941.17
// carol bought ~72.93EUR at the cost of ~70.68GBP
// the offer is partially consumed
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{GBP, UINT64_C(29'941'16770347333), -11}));
// Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB))
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{EUR, UINT64_C(30'049'31517120716), -11}));
}
else
{
// After the auto-bridge offers are consumed, single path
// AMM offer is generated with the limit out taking
// into consideration the transfer fee. This results
// in an overall quality offer matching the limit quality
// and the single path AMM offer is consumed. More
// liquidity is consumed overall in post-amendment.
// Total consumed ~60.68GBP/62.93EUR
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{GBP, UINT64_C(1'060'684828792832), -12},
STAmount{EUR, UINT64_C(1'037'06583722134), -11},
ammAlice.tokens()));
// Consumed offer ~72.93EUR/72.93GBP
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{Amounts{
STAmount{EUR, UINT64_C(27'06583722134028), -14},
STAmount{GBP, UINT64_C(27'06583722134028), -14}}}));
BEAST_EXPECT(expectOffers(env, bob, 0));
BEAST_EXPECT(expectOffers(env, ed, 0));
// Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB))
// * 1.25
// = 88.35 = ~29911.64
// carol bought ~72.93EUR at the cost of ~70.68GBP
// the offer is partially consumed
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{GBP, UINT64_C(29'911'64396400896), -11}));
// Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB))
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{EUR, UINT64_C(30'072'93416277865), -11}));
}
// Initial 2000 + 10 = 2010
BEAST_EXPECT(expectHolding(env, bob, GBP(2'010)));
// Initial 2000 - 10 * 1.25 = 1987.5
BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5)));
},
{{GBP(1'000), EUR(1'100)}},
0,
std::nullopt,
{features});
// Payment and transfer fee
// Scenario:
// Bob sends 125GBP to pay 80EUR to Carol
// Payment execution:
// bob's 125GBP/1.25 = 100GBP
// 100GBP/100EUR AMM offer
// 100EUR/1.25 = 80EUR paid to carol
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(env, gw, {bob}, {GBP(200), EUR(200)}, Fund::Acct);
env(rate(gw, 1.25));
env.close();
env(pay(bob, carol, EUR(100)),
path(~EUR),
sendmax(GBP(125)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(ammAlice.expectBalances(
GBP(1'100), EUR(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, bob, GBP(75)));
BEAST_EXPECT(expectHolding(env, carol, EUR(30'080)));
},
{{GBP(1'000), EUR(1'100)}},
0,
std::nullopt,
{features});
// Payment and transfer fee, multiple steps
// Scenario:
// Dan's offer 200CAN/200GBP
// AMM 1000GBP/10125EUR
// Ed's offer 200EUR/200USD
// Bob sends 195.3125CAN to pay 100USD to Carol
// Payment execution:
// bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer
// 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
// 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer
// 100EUR/100USD 100USD/1.25 = 80USD paid to carol
testAMM(
[&](AMM& ammAlice, Env& env) {
Account const dan("dan");
Account const ed("ed");
auto const CAN = gw["CAN"];
fund(env, gw, {dan}, {CAN(200), GBP(200)}, Fund::Acct);
fund(env, gw, {ed}, {EUR(200), USD(200)}, Fund::Acct);
fund(env, gw, {bob}, {CAN(195.3125)}, Fund::Acct);
env(trust(carol, USD(100)));
env(rate(gw, 1.25));
env.close();
env(offer(dan, CAN(200), GBP(200)));
env(offer(ed, EUR(200), USD(200)));
env.close();
env(pay(bob, carol, USD(100)),
path(~GBP, ~EUR, ~USD),
sendmax(CAN(195.3125)),
txflags(tfPartialPayment));
env.close();
BEAST_EXPECT(expectHolding(env, bob, CAN(0)));
BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75)));
BEAST_EXPECT(ammAlice.expectBalances(
GBP(10'125), EUR(10'000), ammAlice.tokens()));
BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100)));
BEAST_EXPECT(expectHolding(env, carol, USD(80)));
},
{{GBP(10'000), EUR(10'125)}},
0,
std::nullopt,
{features});
// Pay amounts close to one side of the pool
testAMM(
[&](AMM& ammAlice, Env& env) {
env(pay(alice, carol, USD(99.99)),
path(~USD),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(tesSUCCESS));
env(pay(alice, carol, USD(100)),
path(~USD),
sendmax(XRP(1)),
txflags(tfPartialPayment),
ter(tesSUCCESS));
env(pay(alice, carol, XRP(100)),
path(~XRP),
sendmax(USD(1)),
txflags(tfPartialPayment),
ter(tesSUCCESS));
env(pay(alice, carol, STAmount{xrpIssue(), 99'999'900}),
path(~XRP),
sendmax(USD(1)),
txflags(tfPartialPayment),
ter(tesSUCCESS));
},
{{XRP(100), USD(100)}},
0,
std::nullopt,
{features});
// Multiple paths/steps
{
Env env(*this, features);
auto const ETH = gw["ETH"];
fund(
env,
gw,
{alice},
XRP(100'000),
{EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
fund(env, gw, {carol, bob}, XRP(1'000), {USD(200)}, Fund::Acct);
AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
AMM xrp_usd(env, alice, XRP(10'150), USD(10'200));
AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
AMM eur_usd(env, alice, EUR(10'100), USD(10'000));
env(pay(bob, carol, USD(100)),
path(~EUR, ~BTC, ~USD),
path(~USD),
path(~ETH, ~EUR, ~USD),
sendmax(XRP(200)));
if (!features[fixAMMv1_1])
{
// XRP-ETH-EUR-USD
// This path provides ~26.06USD/26.2XRP
BEAST_EXPECT(xrp_eth.expectBalances(
XRPAmount(10'026'208'900),
STAmount{ETH, UINT64_C(10'073'65779244494), -11},
xrp_eth.tokens()));
BEAST_EXPECT(eth_eur.expectBalances(
STAmount{ETH, UINT64_C(10'926'34220755506), -11},
STAmount{EUR, UINT64_C(10'973'54232078752), -11},
eth_eur.tokens()));
BEAST_EXPECT(eur_usd.expectBalances(
STAmount{EUR, UINT64_C(10'126'45767921248), -11},
STAmount{USD, UINT64_C(9'973'93151712086), -11},
eur_usd.tokens()));
// XRP-USD path
// This path provides ~73.9USD/74.1XRP
BEAST_EXPECT(xrp_usd.expectBalances(
XRPAmount(10'224'106'246),
STAmount{USD, UINT64_C(10'126'06848287914), -11},
xrp_usd.tokens()));
}
else
{
BEAST_EXPECT(xrp_eth.expectBalances(
XRPAmount(10'026'208'900),
STAmount{ETH, UINT64_C(10'073'65779244461), -11},
xrp_eth.tokens()));
BEAST_EXPECT(eth_eur.expectBalances(
STAmount{ETH, UINT64_C(10'926'34220755539), -11},
STAmount{EUR, UINT64_C(10'973'5423207872), -10},
eth_eur.tokens()));
BEAST_EXPECT(eur_usd.expectBalances(
STAmount{EUR, UINT64_C(10'126'4576792128), -10},
STAmount{USD, UINT64_C(9'973'93151712057), -11},
eur_usd.tokens()));
// XRP-USD path
// This path provides ~73.9USD/74.1XRP
BEAST_EXPECT(xrp_usd.expectBalances(
XRPAmount(10'224'106'246),
STAmount{USD, UINT64_C(10'126'06848287943), -11},
xrp_usd.tokens()));
}
// XRP-EUR-BTC-USD
// This path doesn't provide any liquidity due to how
// offers are generated in multi-path. Analytical solution
// shows a different distribution:
// XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP,
// XRP-ETH-EUR-USD 27.6USD/27.6XRP
BEAST_EXPECT(xrp_eur.expectBalances(
XRP(10'100), EUR(10'000), xrp_eur.tokens()));
BEAST_EXPECT(eur_btc.expectBalances(
EUR(10'000), BTC(10'200), eur_btc.tokens()));
BEAST_EXPECT(btc_usd.expectBalances(
BTC(10'100), USD(10'000), btc_usd.tokens()));
BEAST_EXPECT(expectHolding(env, carol, USD(300)));
}
// Dependent AMM
{
Env env(*this, features);
auto const ETH = gw["ETH"];
fund(
env,
gw,
{alice},
XRP(40'000),
{EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
fund(env, gw, {carol, bob}, XRP(1000), {USD(200)}, Fund::Acct);
AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
env(pay(bob, carol, USD(100)),
path(~EUR, ~BTC, ~USD),
path(~ETH, ~EUR, ~BTC, ~USD),
sendmax(XRP(200)));
if (!features[fixAMMv1_1])
{
// XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP
// XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP
BEAST_EXPECT(xrp_eur.expectBalances(
XRPAmount(10'118'738'472),
STAmount{EUR, UINT64_C(9'981'544436337968), -12},
xrp_eur.tokens()));
BEAST_EXPECT(eur_btc.expectBalances(
STAmount{EUR, UINT64_C(10'101'16096785173), -11},
STAmount{BTC, UINT64_C(10'097'91426968066), -11},
eur_btc.tokens()));
BEAST_EXPECT(btc_usd.expectBalances(
STAmount{BTC, UINT64_C(10'202'08573031934), -11},
USD(9'900),
btc_usd.tokens()));
BEAST_EXPECT(xrp_eth.expectBalances(
XRPAmount(10'082'446'397),
STAmount{ETH, UINT64_C(10'017'41072778012), -11},
xrp_eth.tokens()));
BEAST_EXPECT(eth_eur.expectBalances(
STAmount{ETH, UINT64_C(10'982'58927221988), -11},
STAmount{EUR, UINT64_C(10'917'2945958103), -10},
eth_eur.tokens()));
}
else
{
BEAST_EXPECT(xrp_eur.expectBalances(
XRPAmount(10'118'738'472),
STAmount{EUR, UINT64_C(9'981'544436337923), -12},
xrp_eur.tokens()));
BEAST_EXPECT(eur_btc.expectBalances(
STAmount{EUR, UINT64_C(10'101'16096785188), -11},
STAmount{BTC, UINT64_C(10'097'91426968059), -11},
eur_btc.tokens()));
BEAST_EXPECT(btc_usd.expectBalances(
STAmount{BTC, UINT64_C(10'202'08573031941), -11},
USD(9'900),
btc_usd.tokens()));
BEAST_EXPECT(xrp_eth.expectBalances(
XRPAmount(10'082'446'397),
STAmount{ETH, UINT64_C(10'017'41072777996), -11},
xrp_eth.tokens()));
BEAST_EXPECT(eth_eur.expectBalances(
STAmount{ETH, UINT64_C(10'982'58927222004), -11},
STAmount{EUR, UINT64_C(10'917'2945958102), -10},
eth_eur.tokens()));
}
BEAST_EXPECT(expectHolding(env, carol, USD(300)));
}
// AMM offers limit
// Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
env(trust(alice, EUR(200)));
for (int i = 0; i < 30; ++i)
env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
// This is worse quality offer than 30 offers above.
// It will not be consumed because of AMM offers limit.
env(offer(alice, EUR(140), XRP(100)));
env(pay(bob, carol, USD(100)),
path(~XRP, ~USD),
sendmax(EUR(400)),
txflags(tfPartialPayment | tfNoRippleDirect));
if (!features[fixAMMv1_1])
{
// Carol gets ~29.91USD because of the AMM offers limit
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'030),
STAmount{USD, UINT64_C(9'970'089730807577), -12},
ammAlice.tokens()));
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'029'91026919241), -11}));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'030),
STAmount{USD, UINT64_C(9'970'089730807827), -12},
ammAlice.tokens()));
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'029'91026919217), -11}));
}
BEAST_EXPECT(
expectOffers(env, alice, 1, {{{EUR(140), XRP(100)}}}));
},
std::nullopt,
0,
std::nullopt,
{features});
// This payment is fulfilled
testAMM(
[&](AMM& ammAlice, Env& env) {
env.fund(XRP(1'000), bob);
fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
env(trust(alice, EUR(200)));
for (int i = 0; i < 29; ++i)
env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
// This is worse quality offer than 30 offers above.
// It will not be consumed because of AMM offers limit.
env(offer(alice, EUR(140), XRP(100)));
env(pay(bob, carol, USD(100)),
path(~XRP, ~USD),
sendmax(EUR(400)),
txflags(tfPartialPayment | tfNoRippleDirect));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens()));
if (!features[fixAMMv1_1])
{
// Carol gets ~100USD
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'099'99999999999), -11}));
}
else
{
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
}
BEAST_EXPECT(expectOffers(
env,
alice,
1,
{{{STAmount{EUR, UINT64_C(39'1858572), -7},
XRPAmount{27'989'898}}}}));
},
std::nullopt,
0,
std::nullopt,
{features});
// Offer crossing with AMM and another offer. AMM has a better
// quality and is consumed first.
{
Env env(*this, features);
fund(env, gw, {alice, carol, bob}, XRP(30'000), {USD(30'000)});
env(offer(bob, XRP(100), USD(100.001)));
AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
env(offer(carol, USD(100), XRP(100)));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'049'825'373},
STAmount{USD, UINT64_C(10'049'92586949302), -11},
ammAlice.tokens()));
BEAST_EXPECT(expectOffers(
env,
bob,
1,
{{{XRPAmount{50'074'629},
STAmount{USD, UINT64_C(50'07513050698), -11}}}}));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'049'825'372},
STAmount{USD, UINT64_C(10'049'92587049303), -11},
ammAlice.tokens()));
BEAST_EXPECT(expectOffers(
env,
bob,
1,
{{{XRPAmount{50'074'628},
STAmount{USD, UINT64_C(50'07512950697), -11}}}}));
BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
}
}
// Individually frozen account
testAMM(
[&](AMM& ammAlice, Env& env) {
env(trust(gw, carol["USD"](0), tfSetFreeze));
env(trust(gw, alice["USD"](0), tfSetFreeze));
env.close();
env(pay(alice, carol, USD(1)),
path(~USD),
sendmax(XRP(10)),
txflags(tfNoRippleDirect | tfPartialPayment),
ter(tesSUCCESS));
},
std::nullopt,
0,
std::nullopt,
{features});
}
void
testAMMTokens()
{
testcase("AMM Tokens");
using namespace jtx;
// Offer crossing with AMM LPTokens and XRP.
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base.drops();
auto const token1 = ammAlice.lptIssue();
auto priceXRP = ammAssetOut(
STAmount{XRPAmount{10'000'000'000}},
STAmount{token1, 10'000'000},
STAmount{token1, 5'000'000},
0);
// Carol places an order to buy LPTokens
env(offer(carol, STAmount{token1, 5'000'000}, priceXRP));
// Alice places an order to sell LPTokens
env(offer(alice, priceXRP, STAmount{token1, 5'000'000}));
// Pool's LPTokens balance doesn't change
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
// Carol is Liquidity Provider
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{5'000'000}));
BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
// Carol votes
ammAlice.vote(carol, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(500));
ammAlice.vote(carol, 0);
BEAST_EXPECT(ammAlice.expectTradingFee(0));
// Carol bids
env(ammAlice.bid({.account = carol, .bidMin = 100}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900}));
BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
BEAST_EXPECT(
accountBalance(env, carol) ==
std::to_string(22500000000 - 4 * baseFee));
priceXRP = ammAssetOut(
STAmount{XRPAmount{10'000'000'000}},
STAmount{token1, 9'999'900},
STAmount{token1, 4'999'900},
0);
// Carol withdraws
ammAlice.withdrawAll(carol, XRP(0));
BEAST_EXPECT(
accountBalance(env, carol) ==
std::to_string(29999949999 - 5 * baseFee));
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount{10'000'000'000} - priceXRP,
USD(10'000),
IOUAmount{5'000'000}));
BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
});
// Offer crossing with two AMM LPTokens.
testAMM([&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
ammAlice1.deposit(carol, 1'000'000);
auto const token1 = ammAlice.lptIssue();
auto const token2 = ammAlice1.lptIssue();
env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}),
txflags(tfPassive));
env.close();
BEAST_EXPECT(expectOffers(env, alice, 1));
env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
env.close();
BEAST_EXPECT(
expectHolding(env, alice, STAmount{token1, 10'000'100}) &&
expectHolding(env, alice, STAmount{token2, 9'999'900}));
BEAST_EXPECT(
expectHolding(env, carol, STAmount{token2, 1'000'100}) &&
expectHolding(env, carol, STAmount{token1, 999'900}));
BEAST_EXPECT(
expectOffers(env, alice, 0) && expectOffers(env, carol, 0));
});
// LPs pay LPTokens directly. Must trust set because the trust line
// is checked for the limit, which is 0 in the AMM auto-created
// trust line.
testAMM([&](AMM& ammAlice, Env& env) {
auto const token1 = ammAlice.lptIssue();
env.trust(STAmount{token1, 2'000'000}, carol);
env.close();
ammAlice.deposit(carol, 1'000'000);
BEAST_EXPECT(
ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
// Pool balance doesn't change, only tokens moved from
// one line to another.
env(pay(alice, carol, STAmount{token1, 100}));
env.close();
BEAST_EXPECT(
// Alice initial token1 10,000,000 - 100
ammAlice.expectLPTokens(alice, IOUAmount{9'999'900, 0}) &&
// Carol initial token1 1,000,000 + 100
ammAlice.expectLPTokens(carol, IOUAmount{1'000'100, 0}));
env.trust(STAmount{token1, 20'000'000}, alice);
env.close();
env(pay(carol, alice, STAmount{token1, 100}));
env.close();
// Back to the original balance
BEAST_EXPECT(
ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
});
}
void
testAmendment()
{
testcase("Amendment");
using namespace jtx;
FeatureBitset const all{testable_amendments()};
FeatureBitset const noAMM{all - featureAMM};
FeatureBitset const noNumber{all - fixUniversalNumber};
FeatureBitset const noAMMAndNumber{
all - featureAMM - fixUniversalNumber};
for (auto const& feature : {noAMM, noNumber, noAMMAndNumber})
{
Env env{*this, feature};
fund(env, gw, {alice}, {USD(1'000)}, Fund::All);
AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED));
env(amm.bid({.bidMax = 1000}), ter(temMALFORMED));
env(amm.bid({}), ter(temDISABLED));
amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)});
amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)});
amm.withdraw(WithdrawArg{.err = ter(temDISABLED)});
amm.deposit(
DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)});
amm.ammDelete(alice, ter(temDISABLED));
}
}
void
testFlags()
{
testcase("Flags");
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
auto const info = env.rpc(
"json",
"account_info",
std::string(
"{\"account\": \"" + to_string(ammAlice.ammAccount()) +
"\"}"));
auto const flags =
info[jss::result][jss::account_data][jss::Flags].asUInt();
BEAST_EXPECT(
flags ==
(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
});
}
void
testRippling()
{
testcase("Rippling");
using namespace jtx;
// Rippling via AMM fails because AMM trust line has 0 limit.
// Set up two issuers, A and B. Have each issue a token called TST.
// Have another account C hold TST from both issuers,
// and create an AMM for this pair.
// Have a fourth account, D, create a trust line to the AMM for TST.
// Send a payment delivering TST.AMM from C to D, using SendMax in
// TST.A (or B) and a path through the AMM account. By normal
// rippling rules, this would have caused the AMM's balances
// to shift at a 1:1 rate with no fee applied has it not been
// for 0 limit.
{
Env env(*this);
auto const A = Account("A");
auto const B = Account("B");
auto const TSTA = A["TST"];
auto const TSTB = B["TST"];
auto const C = Account("C");
auto const D = Account("D");
env.fund(XRP(10'000), A);
env.fund(XRP(10'000), B);
env.fund(XRP(10'000), C);
env.fund(XRP(10'000), D);
env.trust(TSTA(10'000), C);
env.trust(TSTB(10'000), C);
env(pay(A, C, TSTA(10'000)));
env(pay(B, C, TSTB(10'000)));
AMM amm(env, C, TSTA(5'000), TSTB(5'000));
auto const ammIss = Issue(TSTA.currency, amm.ammAccount());
// Can SetTrust only for AMM LP tokens
env(trust(D, STAmount{ammIss, 10'000}), ter(tecNO_PERMISSION));
env.close();
// The payment would fail because of above, but check just in case
env(pay(C, D, STAmount{ammIss, 10}),
sendmax(TSTA(100)),
path(amm.ammAccount()),
txflags(tfPartialPayment | tfNoRippleDirect),
ter(tecPATH_DRY));
}
}
void
testAMMAndCLOB(FeatureBitset features)
{
testcase("AMMAndCLOB, offer quality change");
using namespace jtx;
auto const gw = Account("gw");
auto const TST = gw["TST"];
auto const LP1 = Account("LP1");
auto const LP2 = Account("LP2");
auto prep = [&](auto const& offerCb, auto const& expectCb) {
Env env(*this, features);
env.fund(XRP(30'000'000'000), gw);
env(offer(gw, XRP(11'500'000'000), TST(1'000'000'000)));
env.fund(XRP(10'000), LP1);
env.fund(XRP(10'000), LP2);
env(offer(LP1, TST(25), XRPAmount(287'500'000)));
// Either AMM or CLOB offer
offerCb(env);
env(offer(LP2, TST(25), XRPAmount(287'500'000)));
expectCb(env);
};
// If we replace AMM with an equivalent CLOB offer, which AMM generates
// when it is consumed, then the result must be equivalent, too.
std::string lp2TSTBalance;
std::string lp2TakerGets;
std::string lp2TakerPays;
// Execute with AMM first
prep(
[&](Env& env) { AMM amm(env, LP1, TST(25), XRP(250)); },
[&](Env& env) {
lp2TSTBalance =
getAccountLines(env, LP2, TST)["lines"][0u]["balance"]
.asString();
auto const offer = getAccountOffers(env, LP2)["offers"][0u];
lp2TakerGets = offer["taker_gets"].asString();
lp2TakerPays = offer["taker_pays"]["value"].asString();
});
// Execute with CLOB offer
prep(
[&](Env& env) {
if (!features[fixAMMv1_1])
env(offer(
LP1,
XRPAmount{18'095'133},
STAmount{TST, UINT64_C(1'68737984885388), -14}),
txflags(tfPassive));
else
env(offer(
LP1,
XRPAmount{18'095'132},
STAmount{TST, UINT64_C(1'68737976189735), -14}),
txflags(tfPassive));
},
[&](Env& env) {
BEAST_EXPECT(
lp2TSTBalance ==
getAccountLines(env, LP2, TST)["lines"][0u]["balance"]
.asString());
auto const offer = getAccountOffers(env, LP2)["offers"][0u];
BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
BEAST_EXPECT(
lp2TakerPays == offer["taker_pays"]["value"].asString());
});
}
void
testTradingFee(FeatureBitset features)
{
testcase("Trading Fee");
using namespace jtx;
// Single Deposit, 1% fee
testAMM(
[&](AMM& ammAlice, Env& env) {
// No fee
ammAlice.deposit(carol, USD(3'000));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
ammAlice.withdrawAll(carol, USD(3'000));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
// Set fee to 1%
ammAlice.vote(alice, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
// Carol gets fewer LPToken ~994, because of the single deposit
// fee
ammAlice.deposit(carol, USD(3'000));
BEAST_EXPECT(ammAlice.expectLPTokens(
carol, IOUAmount{994'981155689671, -12}));
BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
// Set fee to 0
ammAlice.vote(alice, 0);
ammAlice.withdrawAll(carol, USD(0));
// Carol gets back less than the original deposit
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(29'994'96220068281), -11}));
},
{{USD(1'000), EUR(1'000)}},
0,
std::nullopt,
{features});
// Single deposit with EP not exceeding specified:
// 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const balance = env.balance(carol, USD);
auto tokensFee = ammAlice.deposit(
carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
auto const deposit = balance - env.balance(carol, USD);
ammAlice.withdrawAll(carol, USD(0));
ammAlice.vote(alice, 0);
BEAST_EXPECT(ammAlice.expectTradingFee(0));
auto const tokensNoFee = ammAlice.deposit(carol, deposit);
// carol pays ~2008 LPTokens in fees or ~0.5% of the no-fee
// LPTokens
BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7));
BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8));
},
std::nullopt,
1'000,
std::nullopt,
{features});
// Single deposit with EP not exceeding specified:
// 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
testAMM(
[&](AMM& ammAlice, Env& env) {
auto const balance = env.balance(carol, USD);
auto const tokensFee = ammAlice.deposit(
carol, USD(200), std::nullopt, STAmount{USD, 2020, -6});
auto const deposit = balance - env.balance(carol, USD);
ammAlice.withdrawAll(carol, USD(0));
ammAlice.vote(alice, 0);
BEAST_EXPECT(ammAlice.expectTradingFee(0));
auto const tokensNoFee = ammAlice.deposit(carol, deposit);
// carol pays ~475 LPTokens in fees or ~0.5% of the no-fee
// LPTokens
BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8));
BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8));
},
std::nullopt,
1'000,
std::nullopt,
{features});
// Single Withdrawal, 1% fee
testAMM(
[&](AMM& ammAlice, Env& env) {
// No fee
ammAlice.deposit(carol, USD(3'000));
BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
// Set fee to 1%
ammAlice.vote(alice, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
// Single withdrawal. Carol gets ~5USD less than deposited.
ammAlice.withdrawAll(carol, USD(0));
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(29'994'97487437186), -11}));
},
{{USD(1'000), EUR(1'000)}},
0,
std::nullopt,
{features});
// Withdraw with EPrice limit, 1% fee.
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.deposit(carol, 1'000'000);
auto const tokensFee = ammAlice.withdraw(
carol, USD(100), std::nullopt, IOUAmount{520, 0});
// carol withdraws ~1,443.44USD
auto const balanceAfterWithdraw = [&]() {
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
return STAmount(USD, UINT64_C(30'443'43891402715), -11);
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
return STAmount(USD, UINT64_C(30'443'43891402714), -11);
else
return STAmount(USD, UINT64_C(30'443'43891402713), -11);
}();
BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw);
// Set to original pool size
auto const deposit = balanceAfterWithdraw - USD(29'000);
ammAlice.deposit(carol, deposit);
// fee 0%
ammAlice.vote(alice, 0);
BEAST_EXPECT(ammAlice.expectTradingFee(0));
auto const tokensNoFee = ammAlice.withdraw(carol, deposit);
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(30'443'43891402717), -11));
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(30'443'43891402716), -11));
else
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(30'443'43891402713), -11));
// carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee
// LPTokens
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(
tokensNoFee == IOUAmount(746'579'80779913, -8));
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(
tokensNoFee == IOUAmount(746'579'80779912, -8));
else
BEAST_EXPECT(
tokensNoFee == IOUAmount(746'579'80779911, -8));
BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
},
std::nullopt,
1'000,
std::nullopt,
{features});
// Payment, 1% fee
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(1'000),
{USD(1'000), EUR(1'000)},
Fund::Acct);
// Alice contributed 1010EUR and 1000USD to the pool
BEAST_EXPECT(expectHolding(env, alice, EUR(28'990)));
BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
// Carol pays to Alice with no fee
env(pay(carol, alice, EUR(10)),
path(~EUR),
sendmax(USD(10)),
txflags(tfNoRippleDirect));
env.close();
// Alice has 10EUR more and Carol has 10USD less
BEAST_EXPECT(expectHolding(env, alice, EUR(29'000)));
BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));
// Set fee to 1%
ammAlice.vote(alice, 1'000);
BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
// Bob pays to Carol with 1% fee
env(pay(bob, carol, USD(10)),
path(~USD),
sendmax(EUR(15)),
txflags(tfNoRippleDirect));
env.close();
// Bob sends 10.1~EUR to pay 10USD
BEAST_EXPECT(expectHolding(
env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13}));
// Carol got 10USD
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'000),
STAmount{EUR, UINT64_C(1'010'10101010101), -11},
ammAlice.tokens()));
},
{{USD(1'000), EUR(1'010)}},
0,
std::nullopt,
{features});
// Offer crossing, 0.5% fee
testAMM(
[&](AMM& ammAlice, Env& env) {
// No fee
env(offer(carol, EUR(10), USD(10)));
env.close();
BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));
BEAST_EXPECT(expectHolding(env, carol, EUR(30'010)));
// Change pool composition back
env(offer(carol, USD(10), EUR(10)));
env.close();
// Set fee to 0.5%
ammAlice.vote(alice, 500);
BEAST_EXPECT(ammAlice.expectTradingFee(500));
env(offer(carol, EUR(10), USD(10)));
env.close();
// Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes
// to the pool
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(29'995'02512562814), -11}));
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{EUR, UINT64_C(30'004'97487437186), -11}));
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{{Amounts{
STAmount{EUR, UINT64_C(5'025125628140703), -15},
STAmount{USD, UINT64_C(5'025125628140703), -15}}}}));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{USD, UINT64_C(1'004'974874371859), -12},
STAmount{EUR, UINT64_C(1'005'025125628141), -12},
ammAlice.tokens()));
}
else
{
BEAST_EXPECT(ammAlice.expectBalances(
STAmount{USD, UINT64_C(1'004'97487437186), -11},
STAmount{EUR, UINT64_C(1'005'025125628141), -12},
ammAlice.tokens()));
}
},
{{USD(1'000), EUR(1'010)}},
0,
std::nullopt,
{features});
// Payment with AMM and CLOB offer, 0 fee
// AMM liquidity is consumed first up to CLOB offer quality
// CLOB offer is fully consumed next
// Remaining amount is consumed via AMM liquidity
{
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(2'000), EUR(2'000)});
env(offer(carol, EUR(5), USD(5)));
AMM ammAlice(env, alice, USD(1'005), EUR(1'000));
env(pay(bob, ed, USD(10)),
path(~USD),
sendmax(EUR(15)),
txflags(tfNoRippleDirect));
BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'000), EUR(1'005), ammAlice.tokens()));
}
else
{
BEAST_EXPECT(expectHolding(
env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12)));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'000),
STAmount(EUR, UINT64_C(1005'000000000001), -12),
ammAlice.tokens()));
}
BEAST_EXPECT(expectOffers(env, carol, 0));
}
// Payment with AMM and CLOB offer. Same as above but with 0.25%
// fee.
{
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(2'000), EUR(2'000)});
env(offer(carol, EUR(5), USD(5)));
// Set 0.25% fee
AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 250);
env(pay(bob, ed, USD(10)),
path(~USD),
sendmax(EUR(15)),
txflags(tfNoRippleDirect));
BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(expectHolding(
env,
bob,
STAmount{EUR, UINT64_C(1'989'987453007618), -12}));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'000),
STAmount{EUR, UINT64_C(1'005'012546992382), -12},
ammAlice.tokens()));
}
else
{
BEAST_EXPECT(expectHolding(
env,
bob,
STAmount{EUR, UINT64_C(1'989'987453007628), -12}));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'000),
STAmount{EUR, UINT64_C(1'005'012546992372), -12},
ammAlice.tokens()));
}
BEAST_EXPECT(expectOffers(env, carol, 0));
}
// Payment with AMM and CLOB offer. AMM has a better
// spot price quality, but 1% fee offsets that. As the result
// the entire trade is executed via LOB.
{
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(2'000), EUR(2'000)});
env(offer(carol, EUR(10), USD(10)));
// Set 1% fee
AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
env(pay(bob, ed, USD(10)),
path(~USD),
sendmax(EUR(15)),
txflags(tfNoRippleDirect));
BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'005), EUR(1'000), ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
}
// Payment with AMM and CLOB offer. AMM has a better
// spot price quality, but 1% fee offsets that.
// The CLOB offer is consumed first and the remaining
// amount is consumed via AMM liquidity.
{
Env env(*this, features);
Account const ed("ed");
fund(
env,
gw,
{alice, bob, carol, ed},
XRP(1'000),
{USD(2'000), EUR(2'000)});
env(offer(carol, EUR(9), USD(9)));
// Set 1% fee
AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
env(pay(bob, ed, USD(10)),
path(~USD),
sendmax(EUR(15)),
txflags(tfNoRippleDirect));
BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
BEAST_EXPECT(expectHolding(
env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12}));
BEAST_EXPECT(ammAlice.expectBalances(
USD(1'004),
STAmount{EUR, UINT64_C(1'001'006076703288), -12},
ammAlice.tokens()));
BEAST_EXPECT(expectOffers(env, carol, 0));
}
}
void
testAdjustedTokens(FeatureBitset features)
{
testcase("Adjusted Deposit/Withdraw Tokens");
using namespace jtx;
// Deposit/Withdraw in USD
testAMM(
[&](AMM& ammAlice, Env& env) {
Account const bob("bob");
Account const ed("ed");
Account const paul("paul");
Account const dan("dan");
Account const chris("chris");
Account const simon("simon");
Account const ben("ben");
Account const nataly("nataly");
fund(
env,
gw,
{bob, ed, paul, dan, chris, simon, ben, nataly},
{USD(1'500'000)},
Fund::Acct);
for (int i = 0; i < 10; ++i)
{
ammAlice.deposit(ben, STAmount{USD, 1, -10});
ammAlice.withdrawAll(ben, USD(0));
ammAlice.deposit(simon, USD(0.1));
ammAlice.withdrawAll(simon, USD(0));
ammAlice.deposit(chris, USD(1));
ammAlice.withdrawAll(chris, USD(0));
ammAlice.deposit(dan, USD(10));
ammAlice.withdrawAll(dan, USD(0));
ammAlice.deposit(bob, USD(100));
ammAlice.withdrawAll(bob, USD(0));
ammAlice.deposit(carol, USD(1'000));
ammAlice.withdrawAll(carol, USD(0));
ammAlice.deposit(ed, USD(10'000));
ammAlice.withdrawAll(ed, USD(0));
ammAlice.deposit(paul, USD(100'000));
ammAlice.withdrawAll(paul, USD(0));
ammAlice.deposit(nataly, USD(1'000'000));
ammAlice.withdrawAll(nataly, USD(0));
}
// Due to round off some accounts have a tiny gain, while
// other have a tiny loss. The last account to withdraw
// gets everything in the pool.
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'000'0000000013), -10},
IOUAmount{10'000'000}));
else if (features[fixAMMv1_3])
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000),
STAmount{USD, UINT64_C(10'000'0000000003), -10},
IOUAmount{10'000'000}));
else
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000)));
BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000)));
BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000)));
BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000)));
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(expectHolding(
env,
carol,
STAmount{USD, UINT64_C(30'000'00000000001), -11}));
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
else
BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000)));
BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000)));
if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(expectHolding(
env,
nataly,
STAmount{USD, UINT64_C(1'500'000'000000002), -9}));
else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
BEAST_EXPECT(expectHolding(
env,
nataly,
STAmount{USD, UINT64_C(1'500'000'000000005), -9}));
else
BEAST_EXPECT(expectHolding(env, nataly, USD(1'500'000)));
ammAlice.withdrawAll(alice);
BEAST_EXPECT(!ammAlice.ammExists());
if (!features[fixAMMv1_1])
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{USD, UINT64_C(30'000'0000000013), -10}));
else if (features[fixAMMv1_3])
BEAST_EXPECT(expectHolding(
env,
alice,
STAmount{USD, UINT64_C(30'000'0000000003), -10}));
else
BEAST_EXPECT(expectHolding(env, alice, USD(30'000)));
// alice XRP balance is 30,000initial - 50 ammcreate fee -
// 10drops fee
BEAST_EXPECT(
accountBalance(env, alice) ==
std::to_string(
29950000000 - env.current()->fees().base.drops()));
},
std::nullopt,
0,
std::nullopt,
{features});
// Same as above but deposit/withdraw in XRP
testAMM(
[&](AMM& ammAlice, Env& env) {
Account const bob("bob");
Account const ed("ed");
Account const paul("paul");
Account const dan("dan");
Account const chris("chris");
Account const simon("simon");
Account const ben("ben");
Account const nataly("nataly");
fund(
env,
gw,
{bob, ed, paul, dan, chris, simon, ben, nataly},
XRP(2'000'000),
{},
Fund::Acct);
for (int i = 0; i < 10; ++i)
{
ammAlice.deposit(ben, XRPAmount{1});
ammAlice.withdrawAll(ben, XRP(0));
ammAlice.deposit(simon, XRPAmount(1'000));
ammAlice.withdrawAll(simon, XRP(0));
ammAlice.deposit(chris, XRP(1));
ammAlice.withdrawAll(chris, XRP(0));
ammAlice.deposit(dan, XRP(10));
ammAlice.withdrawAll(dan, XRP(0));
ammAlice.deposit(bob, XRP(100));
ammAlice.withdrawAll(bob, XRP(0));
ammAlice.deposit(carol, XRP(1'000));
ammAlice.withdrawAll(carol, XRP(0));
ammAlice.deposit(ed, XRP(10'000));
ammAlice.withdrawAll(ed, XRP(0));
ammAlice.deposit(paul, XRP(100'000));
ammAlice.withdrawAll(paul, XRP(0));
ammAlice.deposit(nataly, XRP(1'000'000));
ammAlice.withdrawAll(nataly, XRP(0));
}
auto const baseFee = env.current()->fees().base.drops();
if (!features[fixAMMv1_3])
{
// No round off with XRP in this test
BEAST_EXPECT(ammAlice.expectBalances(
XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
ammAlice.withdrawAll(alice);
BEAST_EXPECT(!ammAlice.ammExists());
// 20,000 initial - (deposit+withdraw) * 10
auto const xrpBalance =
(XRP(2'000'000) - txfee(env, 20)).getText();
BEAST_EXPECT(accountBalance(env, ben) == xrpBalance);
BEAST_EXPECT(accountBalance(env, simon) == xrpBalance);
BEAST_EXPECT(accountBalance(env, chris) == xrpBalance);
BEAST_EXPECT(accountBalance(env, dan) == xrpBalance);
// 30,000 initial - (deposit+withdraw) * 10
BEAST_EXPECT(
accountBalance(env, carol) ==
std::to_string(30'000'000'000 - 20 * baseFee));
BEAST_EXPECT(accountBalance(env, ed) == xrpBalance);
BEAST_EXPECT(accountBalance(env, paul) == xrpBalance);
BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance);
// 30,000 initial - 50 ammcreate fee - 10drops withdraw fee
BEAST_EXPECT(
accountBalance(env, alice) ==
std::to_string(29'950'000'000 - baseFee));
}
else
{
// post-amendment the rounding takes place to ensure
// AMM invariant
BEAST_EXPECT(ammAlice.expectBalances(
XRPAmount(10'000'000'080),
USD(10'000),
IOUAmount{10'000'000}));
ammAlice.withdrawAll(alice);
BEAST_EXPECT(!ammAlice.ammExists());
auto const xrpBalance =
XRP(2'000'000) - txfee(env, 20) - drops(10);
auto const xrpBalanceText = xrpBalance.getText();
BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText);
BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText);
BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText);
BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText);
BEAST_EXPECT(
accountBalance(env, carol) ==
std::to_string(30'000'000'000 - 20 * baseFee - 10));
BEAST_EXPECT(
accountBalance(env, ed) ==
(xrpBalance + drops(2)).getText());
BEAST_EXPECT(
accountBalance(env, paul) ==
(xrpBalance + drops(3)).getText());
BEAST_EXPECT(
accountBalance(env, nataly) ==
(xrpBalance + drops(5)).getText());
BEAST_EXPECT(
accountBalance(env, alice) ==
std::to_string(29'950'000'000 - baseFee + 80));
}
},
std::nullopt,
0,
std::nullopt,
{features});
}
void
testAutoDelete()
{
testcase("Auto Delete");
using namespace jtx;
FeatureBitset const all{testable_amendments()};
{
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->FEES.reference_fee = XRPAmount(1);
return cfg;
}),
all);
fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
AMM amm(env, gw, XRP(10'000), USD(10'000));
for (auto i = 0; i < maxDeletableAMMTrustLines + 10; ++i)
{
Account const a{std::to_string(i)};
env.fund(XRP(1'000), a);
env(trust(a, STAmount{amm.lptIssue(), 10'000}));
env.close();
}
// The trustlines are partially deleted,
// AMM is set to an empty state.
amm.withdrawAll(gw);
BEAST_EXPECT(amm.ammExists());
// Bid,Vote,Deposit,Withdraw,SetTrust failing with
// tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
env(amm.bid({
.account = alice,
.bidMin = 1000,
}),
ter(tecAMM_EMPTY));
amm.vote(
std::nullopt,
100,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_EMPTY));
amm.withdraw(
alice, 100, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
amm.deposit(
alice,
USD(100),
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_EMPTY));
env(trust(alice, STAmount{amm.lptIssue(), 10'000}),
ter(tecAMM_EMPTY));
// Can deposit with tfTwoAssetIfEmpty option
amm.deposit(
alice,
std::nullopt,
XRP(10'000),
USD(10'000),
std::nullopt,
tfTwoAssetIfEmpty,
std::nullopt,
std::nullopt,
1'000);
BEAST_EXPECT(
amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens()));
BEAST_EXPECT(amm.expectTradingFee(1'000));
BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
// Withdrawing all tokens deletes AMM since the number
// of remaining trustlines is less than max
amm.withdrawAll(alice);
BEAST_EXPECT(!amm.ammExists());
BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
}
{
Env env(
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->FEES.reference_fee = XRPAmount(1);
return cfg;
}),
all);
fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
AMM amm(env, gw, XRP(10'000), USD(10'000));
for (auto i = 0; i < maxDeletableAMMTrustLines * 2 + 10; ++i)
{
Account const a{std::to_string(i)};
env.fund(XRP(1'000), a);
env(trust(a, STAmount{amm.lptIssue(), 10'000}));
env.close();
}
// The trustlines are partially deleted.
amm.withdrawAll(gw);
BEAST_EXPECT(amm.ammExists());
// AMMDelete has to be called twice to delete AMM.
amm.ammDelete(alice, ter(tecINCOMPLETE));
BEAST_EXPECT(amm.ammExists());
// Deletes remaining trustlines and deletes AMM.
amm.ammDelete(alice);
BEAST_EXPECT(!amm.ammExists());
BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
// Try redundant delete
amm.ammDelete(alice, ter(terNO_AMM));
}
}
void
testClawback()
{
testcase("Clawback");
using namespace jtx;
Env env(*this);
env.fund(XRP(2'000), gw);
env.fund(XRP(2'000), alice);
AMM amm(env, gw, XRP(1'000), USD(1'000));
env(fset(gw, asfAllowTrustLineClawback), ter(tecOWNERS));
}
void
testAMMID()
{
testcase("AMMID");
using namespace jtx;
testAMM([&](AMM& amm, Env& env) {
amm.setClose(false);
auto const info = env.rpc(
"json",
"account_info",
std::string(
"{\"account\": \"" + to_string(amm.ammAccount()) + "\"}"));
try
{
BEAST_EXPECT(
info[jss::result][jss::account_data][jss::AMMID]
.asString() == to_string(amm.ammID()));
}
catch (...)
{
fail();
}
amm.deposit(carol, 1'000);
auto affected = env.meta()->getJson(
JsonOptions::none)[sfAffectedNodes.fieldName];
try
{
bool found = false;
for (auto const& node : affected)
{
if (node.isMember(sfModifiedNode.fieldName) &&
node[sfModifiedNode.fieldName]
[sfLedgerEntryType.fieldName]
.asString() == "AccountRoot" &&
node[sfModifiedNode.fieldName][sfFinalFields.fieldName]
[jss::Account]
.asString() == to_string(amm.ammAccount()))
{
found = node[sfModifiedNode.fieldName]
[sfFinalFields.fieldName][jss::AMMID]
.asString() == to_string(amm.ammID());
break;
}
}
BEAST_EXPECT(found);
}
catch (...)
{
fail();
}
});
}
void
testSelection(FeatureBitset features)
{
testcase("Offer/Strand Selection");
using namespace jtx;
Account const ed("ed");
Account const gw1("gw1");
auto const ETH = gw1["ETH"];
auto const CAN = gw1["CAN"];
// These tests are expected to fail if the OwnerPaysFee feature
// is ever supported. Updates will need to be made to AMM handling
// in the payment engine, and these tests will need to be updated.
auto prep = [&](Env& env, auto gwRate, auto gw1Rate) {
fund(env, gw, {alice, carol, bob, ed}, XRP(2'000), {USD(2'000)});
env.fund(XRP(2'000), gw1);
fund(
env,
gw1,
{alice, carol, bob, ed},
{ETH(2'000), CAN(2'000)},
Fund::IOUOnly);
env(rate(gw, gwRate));
env(rate(gw1, gw1Rate));
env.close();
};
for (auto const& rates :
{std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)})
{
// Offer Selection
// Cross-currency payment: AMM has the same spot price quality
// as CLOB's offer and can't generate a better quality offer.
// The transfer fee in this case doesn't change the CLOB quality
// because trIn is ignored on adjustment and trOut on payment is
// also ignored because ownerPaysTransferFee is false in this
// case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
// verify that the quality is better in the first case, and CLOB
// is selected in the second case.
{
std::array<Quality, 3> q;
for (auto i = 0; i < 3; ++i)
{
Env env(*this, features);
prep(env, rates.first, rates.second);
std::optional<AMM> amm;
if (i == 0 || i == 2)
{
env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
env.close();
}
if (i > 0)
amm.emplace(env, ed, USD(1'000), ETH(1'000));
env(pay(carol, bob, USD(100)),
path(~USD),
sendmax(ETH(500)));
env.close();
// CLOB and AMM, AMM is not selected
if (i == 2)
{
BEAST_EXPECT(amm->expectBalances(
USD(1'000), ETH(1'000), amm->tokens()));
}
BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
q[i] = Quality(Amounts{
ETH(2'000) - env.balance(carol, ETH),
env.balance(bob, USD) - USD(2'000)});
}
// CLOB is better quality than AMM
BEAST_EXPECT(q[0] > q[1]);
// AMM is not selected with CLOB
BEAST_EXPECT(q[0] == q[2]);
}
// Offer crossing: AMM has the same spot price quality
// as CLOB's offer and can't generate a better quality offer.
// The transfer fee in this case doesn't change the CLOB quality
// because the quality adjustment is ignored for the offer
// crossing.
for (auto i = 0; i < 3; ++i)
{
Env env(*this, features);
prep(env, rates.first, rates.second);
std::optional<AMM> amm;
if (i == 0 || i == 2)
{
env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
env.close();
}
if (i > 0)
amm.emplace(env, ed, USD(1'000), ETH(1'000));
env(offer(alice, USD(400), ETH(400)));
env.close();
// AMM is not selected
if (i > 0)
{
BEAST_EXPECT(amm->expectBalances(
USD(1'000), ETH(1'000), amm->tokens()));
}
if (i == 0 || i == 2)
{
// Fully crosses
BEAST_EXPECT(expectOffers(env, alice, 0));
}
// Fails to cross because AMM is not selected
else
{
BEAST_EXPECT(expectOffers(
env, alice, 1, {Amounts{USD(400), ETH(400)}}));
}
BEAST_EXPECT(expectOffers(env, ed, 0));
}
// Show that the CLOB quality reduction
// results in AMM offer selection.
// Same as the payment but reduced offer quality
{
std::array<Quality, 3> q;
for (auto i = 0; i < 3; ++i)
{
Env env(*this, features);
prep(env, rates.first, rates.second);
std::optional<AMM> amm;
if (i == 0 || i == 2)
{
env(offer(ed, ETH(400), USD(300)), txflags(tfPassive));
env.close();
}
if (i > 0)
amm.emplace(env, ed, USD(1'000), ETH(1'000));
env(pay(carol, bob, USD(100)),
path(~USD),
sendmax(ETH(500)));
env.close();
// AMM and CLOB are selected
if (i > 0)
{
BEAST_EXPECT(!amm->expectBalances(
USD(1'000), ETH(1'000), amm->tokens()));
}
if (i == 2 && !features[fixAMMv1_1])
{
if (rates.first == 1.5)
{
if (!features[fixAMMv1_1])
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH,
UINT64_C(378'6327949540823),
-13},
STAmount{
USD,
UINT64_C(283'9745962155617),
-13}}}}));
else
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH,
UINT64_C(378'6327949540813),
-13},
STAmount{
USD,
UINT64_C(283'974596215561),
-12}}}}));
}
else
{
if (!features[fixAMMv1_1])
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH,
UINT64_C(325'299461620749),
-12},
STAmount{
USD,
UINT64_C(243'9745962155617),
-13}}}}));
else
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH,
UINT64_C(325'299461620748),
-12},
STAmount{
USD,
UINT64_C(243'974596215561),
-12}}}}));
}
}
else if (i == 2)
{
if (rates.first == 1.5)
{
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH, UINT64_C(378'6327949540812), -13},
STAmount{
USD,
UINT64_C(283'9745962155609),
-13}}}}));
}
else
{
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH, UINT64_C(325'2994616207479), -13},
STAmount{
USD,
UINT64_C(243'9745962155609),
-13}}}}));
}
}
BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
q[i] = Quality(Amounts{
ETH(2'000) - env.balance(carol, ETH),
env.balance(bob, USD) - USD(2'000)});
}
// AMM is better quality
BEAST_EXPECT(q[1] > q[0]);
// AMM and CLOB produce better quality
BEAST_EXPECT(q[2] > q[1]);
}
// Same as the offer-crossing but reduced offer quality
for (auto i = 0; i < 3; ++i)
{
Env env(*this, features);
prep(env, rates.first, rates.second);
std::optional<AMM> amm;
if (i == 0 || i == 2)
{
env(offer(ed, ETH(400), USD(250)), txflags(tfPassive));
env.close();
}
if (i > 0)
amm.emplace(env, ed, USD(1'000), ETH(1'000));
env(offer(alice, USD(250), ETH(400)));
env.close();
// AMM is selected in both cases
if (i > 0)
{
BEAST_EXPECT(!amm->expectBalances(
USD(1'000), ETH(1'000), amm->tokens()));
}
// Partially crosses, AMM is selected, CLOB fails
// limitQuality
if (i == 2)
{
if (rates.first == 1.5)
{
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(expectOffers(
env, ed, 1, {{Amounts{ETH(400), USD(250)}}}));
BEAST_EXPECT(expectOffers(
env,
alice,
1,
{{Amounts{
STAmount{
USD, UINT64_C(40'5694150420947), -13},
STAmount{
ETH, UINT64_C(64'91106406735152), -14},
}}}));
}
else
{
// Ed offer is partially crossed.
// The updated rounding makes limitQuality
// work if both amendments are enabled
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH, UINT64_C(335'0889359326475), -13},
STAmount{
USD, UINT64_C(209'4305849579047), -13},
}}}));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
}
else
{
if (!features[fixAMMv1_1])
{
// Ed offer is partially crossed.
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH, UINT64_C(335'0889359326485), -13},
STAmount{
USD, UINT64_C(209'4305849579053), -13},
}}}));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
else
{
// Ed offer is partially crossed.
BEAST_EXPECT(expectOffers(
env,
ed,
1,
{{Amounts{
STAmount{
ETH, UINT64_C(335'0889359326475), -13},
STAmount{
USD, UINT64_C(209'4305849579047), -13},
}}}));
BEAST_EXPECT(expectOffers(env, alice, 0));
}
}
}
}
// Strand selection
// Two book steps strand quality is 1.
// AMM strand's best quality is equal to AMM's spot price
// quality, which is 1. Both strands (steps) are adjusted
// for the transfer fee in qualityUpperBound. In case
// of two strands, AMM offers have better quality and are
// consumed first, remaining liquidity is generated by CLOB
// offers. Liquidity from two strands is better in this case
// than in case of one strand with two book steps. Liquidity
// from one strand with AMM has better quality than either one
// strand with two book steps or two strands. It may appear
// unintuitive, but one strand with AMM is optimized and
// generates one AMM offer, while in case of two strands,
// multiple AMM offers are generated, which results in slightly
// worse overall quality.
{
std::array<Quality, 3> q;
for (auto i = 0; i < 3; ++i)
{
Env env(*this, features);
prep(env, rates.first, rates.second);
std::optional<AMM> amm;
if (i == 0 || i == 2)
{
env(offer(ed, ETH(400), CAN(400)), txflags(tfPassive));
env(offer(ed, CAN(400), USD(400))), txflags(tfPassive);
env.close();
}
if (i > 0)
amm.emplace(env, ed, ETH(1'000), USD(1'000));
env(pay(carol, bob, USD(100)),
path(~USD),
path(~CAN, ~USD),
sendmax(ETH(600)));
env.close();
BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
if (i == 2 && !features[fixAMMv1_1])
{
if (rates.first == 1.5)
{
// Liquidity is consumed from AMM strand only
BEAST_EXPECT(amm->expectBalances(
STAmount{ETH, UINT64_C(1'176'66038955758), -11},
USD(850),
amm->tokens()));
}
else
{
BEAST_EXPECT(amm->expectBalances(
STAmount{
ETH, UINT64_C(1'179'540094339627), -12},
STAmount{USD, UINT64_C(847'7880529867501), -13},
amm->tokens()));
BEAST_EXPECT(expectOffers(
env,
ed,
2,
{{Amounts{
STAmount{
ETH,
UINT64_C(343'3179205198749),
-13},
STAmount{
CAN,
UINT64_C(343'3179205198749),
-13},
},
Amounts{
STAmount{
CAN,
UINT64_C(362'2119470132499),
-13},
STAmount{
USD,
UINT64_C(362'2119470132499),
-13},
}}}));
}
}
else if (i == 2)
{
if (rates.first == 1.5)
{
// Liquidity is consumed from AMM strand only
BEAST_EXPECT(amm->expectBalances(
STAmount{
ETH, UINT64_C(1'176'660389557593), -12},
USD(850),
amm->tokens()));
}
else
{
BEAST_EXPECT(amm->expectBalances(
STAmount{ETH, UINT64_C(1'179'54009433964), -11},
STAmount{USD, UINT64_C(847'7880529867501), -13},
amm->tokens()));
BEAST_EXPECT(expectOffers(
env,
ed,
2,
{{Amounts{
STAmount{
ETH,
UINT64_C(343'3179205198749),
-13},
STAmount{
CAN,
UINT64_C(343'3179205198749),
-13},
},
Amounts{
STAmount{
CAN,
UINT64_C(362'2119470132499),
-13},
STAmount{
USD,
UINT64_C(362'2119470132499),
-13},
}}}));
}
}
q[i] = Quality(Amounts{
ETH(2'000) - env.balance(carol, ETH),
env.balance(bob, USD) - USD(2'000)});
}
BEAST_EXPECT(q[1] > q[0]);
BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
}
}
}
void
testFixDefaultInnerObj()
{
testcase("Fix Default Inner Object");
using namespace jtx;
FeatureBitset const all{testable_amendments()};
auto test = [&](FeatureBitset features,
TER const& err1,
TER const& err2,
TER const& err3,
TER const& err4,
std::uint16_t tfee,
bool closeLedger,
std::optional<std::uint16_t> extra = std::nullopt) {
Env env(*this, features);
fund(env, gw, {alice}, XRP(1'000), {USD(10)});
AMM amm(
env,
gw,
XRP(10),
USD(10),
{.tfee = tfee, .close = closeLedger});
amm.deposit(alice, USD(10), XRP(10));
amm.vote(VoteArg{.account = alice, .tfee = tfee, .err = ter(err1)});
amm.withdraw(WithdrawArg{
.account = gw, .asset1Out = USD(1), .err = ter(err2)});
// with the amendment disabled and ledger not closed,
// second vote succeeds if the first vote sets the trading fee
// to non-zero; if the first vote sets the trading fee to >0 &&
// <9 then the second withdraw succeeds if the second vote sets
// the trading fee so that the discounted fee is non-zero
amm.vote(VoteArg{.account = alice, .tfee = 20, .err = ter(err3)});
amm.withdraw(WithdrawArg{
.account = gw, .asset1Out = USD(2), .err = ter(err4)});
};
// ledger is closed after each transaction, vote/withdraw don't fail
// regardless whether the amendment is enabled or not
test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
test(
all - fixInnerObjTemplate,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
0,
true);
// ledger is not closed after each transaction
// vote/withdraw don't fail if the amendment is enabled
test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
// vote/withdraw fail if the amendment is not enabled
// second vote/withdraw still fail: second vote fails because
// the initial trading fee is 0, consequently second withdraw fails
// because the second vote fails
test(
all - fixInnerObjTemplate,
tefEXCEPTION,
tefEXCEPTION,
tefEXCEPTION,
tefEXCEPTION,
0,
false);
// if non-zero trading/discounted fee then vote/withdraw
// don't fail whether the ledger is closed or not and
// the amendment is enabled or not
test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
test(
all - fixInnerObjTemplate,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
10,
true);
test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
test(
all - fixInnerObjTemplate,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
tesSUCCESS,
10,
false);
// non-zero trading fee but discounted fee is 0, vote doesn't fail
// but withdraw fails
test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
// second vote sets the trading fee to non-zero, consequently
// second withdraw doesn't fail even if the amendment is not
// enabled and the ledger is not closed
test(
all - fixInnerObjTemplate,
tesSUCCESS,
tefEXCEPTION,
tesSUCCESS,
tesSUCCESS,
9,
false);
}
void
testFixChangeSpotPriceQuality(FeatureBitset features)
{
testcase("Fix changeSpotPriceQuality");
using namespace jtx;
std::string logs;
enum class Status {
SucceedShouldSucceedResize, // Succeed in pre-fix because
// error allowance, succeed post-fix
// because of offer resizing
FailShouldSucceed, // Fail in pre-fix due to rounding,
// succeed after fix because of XRP
// side is generated first
SucceedShouldFail, // Succeed in pre-fix, fail after fix
// due to small quality difference
Fail, // Both fail because the quality can't be matched
Succeed // Both succeed
};
using enum Status;
auto const xrpIouAmounts10_100 =
TAmounts{XRPAmount{10}, IOUAmount{100}};
auto const iouXrpAmounts10_100 =
TAmounts{IOUAmount{10}, XRPAmount{100}};
// clang-format off
std::vector<std::tuple<std::string, std::string, Quality, std::uint16_t, Status>> tests = {
//Pool In , Pool Out, Quality , Fee, Status
{"0.001519763260828713", "1558701", Quality{5414253689393440221}, 1000, FailShouldSucceed},
{"0.01099814367603737", "1892611", Quality{5482264816516900274}, 1000, FailShouldSucceed},
{"0.78", "796599", Quality{5630392334958379008}, 1000, FailShouldSucceed},
{"105439.2955578965", "49398693", Quality{5910869983721805038}, 400, FailShouldSucceed},
{"12408293.23445213", "4340810521", Quality{5911611095910090752}, 997, FailShouldSucceed},
{"1892611", "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed},
{"423028.8508101858", "3392804520", Quality{5837920340654162816}, 600, FailShouldSucceed},
{"44565388.41001027", "73890647", Quality{6058976634606450001}, 1000, FailShouldSucceed},
{"66831.68494832662", "16", Quality{6346111134641742975}, 0, FailShouldSucceed},
{"675.9287302203422", "1242632304", Quality{5625960929244093294}, 300, FailShouldSucceed},
{"7047.112186735699", "1649845866", Quality{5696855348026306945}, 504, FailShouldSucceed},
{"840236.4402981238", "47419053", Quality{5982561601648018688}, 499, FailShouldSucceed},
{"992715.618909774", "189445631733", Quality{5697835648288106944}, 815, SucceedShouldSucceedResize},
{"504636667521", "185545883.9506651", Quality{6343802275337659280}, 503, SucceedShouldSucceedResize},
{"992706.7218636649", "189447316000", Quality{5697835648288106944}, 797, SucceedShouldSucceedResize},
{"1.068737911388205", "127860278877", Quality{5268604356368739396}, 293, SucceedShouldSucceedResize},
{"17932506.56880419", "189308.6043676173", Quality{6206460598195440068}, 311, SucceedShouldSucceedResize},
{"1.066379294658174", "128042251493", Quality{5268559341368739328}, 270, SucceedShouldSucceedResize},
{"350131413924", "1576879.110907892", Quality{6487411636539049449}, 650, Fail},
{"422093460", "2.731797662057464", Quality{6702911108534394924}, 1000, Fail},
{"76128132223", "367172.7148422662", Quality{6487263463413514240}, 548, Fail},
{"132701839250", "280703770.7695443", Quality{6273750681188885075}, 562, Fail},
{"994165.7604612011", "189551302411", Quality{5697835592690668727}, 815, Fail},
{"45053.33303227917", "86612695359", Quality{5625695218943638190}, 500, Fail},
{"199649.077043865", "14017933007", Quality{5766034667318524880}, 324, Fail},
{"27751824831.70903", "78896950", Quality{6272538159621630432}, 500, Fail},
{"225.3731275781907", "156431793648", Quality{5477818047604078924}, 989, Fail},
{"199649.077043865", "14017933007", Quality{5766036094462806309}, 324, Fail},
{"3.590272027140361", "20677643641", Quality{5406056147042156356}, 808, Fail},
{"1.070884664490231", "127604712776", Quality{5268620608623825741}, 293, Fail},
{"3272.448829820197", "6275124076", Quality{5625710328924117902}, 81, Fail},
{"0.009059512633902926", "7994028", Quality{5477511954775533172}, 1000, Fail},
{"1", "1.0", Quality{0}, 100, Fail},
{"1.0", "1", Quality{0}, 100, Fail},
{"10", "10.0", Quality{xrpIouAmounts10_100}, 100, Fail},
{"10.0", "10", Quality{iouXrpAmounts10_100}, 100, Fail},
{"69864389131", "287631.4543025075", Quality{6487623473313516078}, 451, Succeed},
{"4328342973", "12453825.99247381", Quality{6272522264364865181}, 997, Succeed},
{"32347017", "7003.93031579449", Quality{6347261126087916670}, 1000, Succeed},
{"61697206161", "36631.4583206413", Quality{6558965195382476659}, 500, Succeed},
{"1654524979", "7028.659825511603", Quality{6487551345110052981}, 504, Succeed},
{"88621.22277293179", "5128418948", Quality{5766347291552869205}, 380, Succeed},
{"1892611", "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed},
{"4542.639373338766", "24554809", Quality{5838994982188783710}, 0, Succeed},
{"5132932546", "88542.99750172683", Quality{6419203342950054537}, 380, Succeed},
{"78929964.1549083", "1506494795", Quality{5986890029845558688}, 589, Succeed},
{"10096561906", "44727.72453735605", Quality{6487455290284644551}, 250, Succeed},
{"5092.219565514988", "8768257694", Quality{5626349534958379008}, 503, Succeed},
{"1819778294", "8305.084302902864", Quality{6487429398998540860}, 415, Succeed},
{"6970462.633911943", "57359281", Quality{6054087899185946624}, 850, Succeed},
{"3983448845", "2347.543644281467", Quality{6558965195382476659}, 856, Succeed},
// This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance.
// Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than
// the target quality and the offer fails.
{"771493171", "1.243473020567508", Quality{6707566798038544272}, 100, SucceedShouldFail},
};
// clang-format on
boost::regex rx("^\\d+$");
boost::smatch match;
// tests that succeed should have the same amounts pre-fix and post-fix
std::vector<std::pair<STAmount, STAmount>> successAmounts;
Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
auto rules = env.current()->rules();
CurrentTransactionRulesGuard rg(rules);
for (auto const& t : tests)
{
auto getPool = [&](std::string const& v, bool isXRP) {
if (isXRP)
return amountFromString(xrpIssue(), v);
return amountFromString(noIssue(), v);
};
auto const& quality = std::get<Quality>(t);
auto const tfee = std::get<std::uint16_t>(t);
auto const status = std::get<Status>(t);
auto const poolInIsXRP =
boost::regex_search(std::get<0>(t), match, rx);
auto const poolOutIsXRP =
boost::regex_search(std::get<1>(t), match, rx);
assert(!(poolInIsXRP && poolOutIsXRP));
auto const poolIn = getPool(std::get<0>(t), poolInIsXRP);
auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP);
try
{
auto const amounts = changeSpotPriceQuality(
Amounts{poolIn, poolOut},
quality,
tfee,
env.current()->rules(),
env.journal);
if (amounts)
{
if (status == SucceedShouldSucceedResize)
{
if (!features[fixAMMv1_1])
BEAST_EXPECT(Quality{*amounts} < quality);
else
BEAST_EXPECT(Quality{*amounts} >= quality);
}
else if (status == Succeed)
{
if (!features[fixAMMv1_1])
BEAST_EXPECT(
Quality{*amounts} >= quality ||
withinRelativeDistance(
Quality{*amounts}, quality, Number{1, -7}));
else
BEAST_EXPECT(Quality{*amounts} >= quality);
}
else if (status == FailShouldSucceed)
{
BEAST_EXPECT(
features[fixAMMv1_1] &&
Quality{*amounts} >= quality);
}
else if (status == SucceedShouldFail)
{
BEAST_EXPECT(
!features[fixAMMv1_1] &&
Quality{*amounts} < quality &&
withinRelativeDistance(
Quality{*amounts}, quality, Number{1, -7}));
}
}
else
{
// Fails pre- and post-amendment because the quality can't
// be matched. Verify by generating a tiny offer, which
// doesn't match the quality. Exclude zero quality since
// no offer is generated in this case.
if (status == Fail && quality != Quality{0})
{
auto tinyOffer = [&]() {
if (isXRP(poolIn))
{
auto const takerPays = STAmount{xrpIssue(), 1};
return Amounts{
takerPays,
swapAssetIn(
Amounts{poolIn, poolOut},
takerPays,
tfee)};
}
else if (isXRP(poolOut))
{
auto const takerGets = STAmount{xrpIssue(), 1};
return Amounts{
swapAssetOut(
Amounts{poolIn, poolOut},
takerGets,
tfee),
takerGets};
}
auto const takerPays = toAmount<STAmount>(
getIssue(poolIn), Number{1, -10} * poolIn);
return Amounts{
takerPays,
swapAssetIn(
Amounts{poolIn, poolOut}, takerPays, tfee)};
}();
BEAST_EXPECT(Quality(tinyOffer) < quality);
}
else if (status == FailShouldSucceed)
{
BEAST_EXPECT(!features[fixAMMv1_1]);
}
else if (status == SucceedShouldFail)
{
BEAST_EXPECT(features[fixAMMv1_1]);
}
}
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
!strcmp(e.what(), "changeSpotPriceQuality failed"));
BEAST_EXPECT(
!features[fixAMMv1_1] && status == FailShouldSucceed);
}
}
// Test negative discriminant
{
// b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3
auto const res =
solveQuadraticEqSmallest(Number{1}, Number{1}, Number{1});
BEAST_EXPECT(!res.has_value());
}
}
void
testMalformed()
{
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfSingleAsset,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfOneAssetLPToken,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.flags = tfLimitLPToken,
.err = ter(temMALFORMED),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.asset2Out = XRP(100),
.err = ter(temBAD_AMM_TOKENS),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
WithdrawArg args{
.asset1Out = XRP(100),
.asset2Out = BAD(100),
.err = ter(temBAD_CURRENCY),
};
ammAlice.withdraw(args);
});
testAMM([&](AMM& ammAlice, Env& env) {
Json::Value jv;
jv[jss::TransactionType] = jss::AMMWithdraw;
jv[jss::Flags] = tfLimitLPToken;
jv[jss::Account] = alice.human();
ammAlice.setTokens(jv);
XRP(100).value().setJson(jv[jss::Amount]);
USD(100).value().setJson(jv[jss::EPrice]);
env(jv, ter(temBAD_AMM_TOKENS));
});
}
void
testFixOverflowOffer(FeatureBitset featuresInitial)
{
using namespace jtx;
using namespace std::chrono;
FeatureBitset const all{featuresInitial};
std::string logs;
Account const gatehub{"gatehub"};
Account const bitstamp{"bitstamp"};
Account const trader{"trader"};
auto const usdGH = gatehub["USD"];
auto const btcGH = gatehub["BTC"];
auto const usdBIT = bitstamp["USD"];
struct InputSet
{
char const* testCase;
double const poolUsdBIT;
double const poolUsdGH;
sendmax const sendMaxUsdBIT;
STAmount const sendUsdGH;
STAmount const failUsdGH;
STAmount const failUsdGHr;
STAmount const failUsdBIT;
STAmount const failUsdBITr;
STAmount const goodUsdGH;
STAmount const goodUsdGHr;
STAmount const goodUsdBIT;
STAmount const goodUsdBITr;
IOUAmount const lpTokenBalance;
std::optional<IOUAmount> const lpTokenBalanceAlt = {};
double const offer1BtcGH = 0.1;
double const offer2BtcGH = 0.1;
double const offer2UsdGH = 1;
double const rateBIT = 0.0;
double const rateGH = 0.0;
};
using uint64_t = std::uint64_t;
for (auto const& input : {
InputSet{
.testCase = "Test Fix Overflow Offer", //
.poolUsdBIT = 3, //
.poolUsdGH = 273, //
.sendMaxUsdBIT{usdBIT(50)}, //
.sendUsdGH{usdGH, uint64_t(272'455089820359), -12}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, //
.failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14}, //
.goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, //
.goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13}, //
.goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, //
.goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, //
.lpTokenBalance = {28'61817604250837, -14}, //
.lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, //
.offer1BtcGH = 0.1, //
.offer2BtcGH = 0.1, //
.offer2UsdGH = 1, //
.rateBIT = 1.15, //
.rateGH = 1.2, //
},
InputSet{
.testCase = "Overflow test {1, 100, 0.111}", //
.poolUsdBIT = 1, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(0.111)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(1'111), -3}, //
.failUsdBITr{usdBIT, uint64_t(1'111), -3}, //
.goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, //
.goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14}, //
.goodUsdBIT{usdBIT, uint64_t(1'111), -3}, //
.goodUsdBITr{usdBIT, uint64_t(1'111), -3}, //
.lpTokenBalance{10, 0}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {1, 100, 1.00}", //
.poolUsdBIT = 1, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(1.00)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(2), 0}, //
.failUsdBITr{usdBIT, uint64_t(2), 0}, //
.goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, //
.goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14}, //
.goodUsdBIT{usdBIT, uint64_t(2), 0}, //
.goodUsdBITr{usdBIT, uint64_t(2), 0}, //
.lpTokenBalance{10, 0}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {1, 100, 4.6432}", //
.poolUsdBIT = 1, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(4.6432)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(5'6432), -4}, //
.failUsdBITr{usdBIT, uint64_t(5'6432), -4}, //
.goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
.goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
.lpTokenBalance{10, 0}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {1, 100, 10}", //
.poolUsdBIT = 1, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(10)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(11), 0}, //
.failUsdBITr{usdBIT, uint64_t(11), 0}, //
.goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
.goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
.lpTokenBalance{10, 0}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {50, 100, 5.55}", //
.poolUsdBIT = 50, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(5.55)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(55'55), -2}, //
.failUsdBITr{usdBIT, uint64_t(55'55), -2}, //
.goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, //
.goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13}, //
.goodUsdBIT{usdBIT, uint64_t(55'55), -2}, //
.goodUsdBITr{usdBIT, uint64_t(55'55), -2}, //
.lpTokenBalance{uint64_t(70'71067811865475), -14}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {50, 100, 50.00}", //
.poolUsdBIT = 50, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(50.00)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
.failUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
.failUsdBIT{usdBIT, uint64_t(100), 0}, //
.failUsdBITr{usdBIT, uint64_t(100), 0}, //
.goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
.goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
.goodUsdBIT{usdBIT, uint64_t(100), 0}, //
.goodUsdBITr{usdBIT, uint64_t(100), 0}, //
.lpTokenBalance{uint64_t(70'71067811865475), -14}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {50, 100, 232.16}", //
.poolUsdBIT = 50, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(232.16)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(282'16), -2}, //
.failUsdBITr{usdBIT, uint64_t(282'16), -2}, //
.goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
.goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
.lpTokenBalance{70'71067811865475, -14}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
InputSet{
.testCase = "Overflow test {50, 100, 500}", //
.poolUsdBIT = 50, //
.poolUsdGH = 100, //
.sendMaxUsdBIT{usdBIT(500)}, //
.sendUsdGH{usdGH, 100}, //
.failUsdGH = STAmount{0}, //
.failUsdGHr = STAmount{0}, //
.failUsdBIT{usdBIT, uint64_t(550), 0}, //
.failUsdBITr{usdBIT, uint64_t(550), 0}, //
.goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
.goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
.goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
.lpTokenBalance{70'71067811865475, -14}, //
.offer1BtcGH = 1e-5, //
.offer2BtcGH = 1, //
.offer2UsdGH = 1e-5, //
.rateBIT = 0, //
.rateGH = 0, //
},
})
{
testcase(input.testCase);
for (auto const& features :
{all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all})
{
Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
env.fund(XRP(5'000), gatehub, bitstamp, trader);
env.close();
if (input.rateGH != 0.0)
env(rate(gatehub, input.rateGH));
if (input.rateBIT != 0.0)
env(rate(bitstamp, input.rateBIT));
env(trust(trader, usdGH(10'000'000)));
env(trust(trader, usdBIT(10'000'000)));
env(trust(trader, btcGH(10'000'000)));
env.close();
env(pay(gatehub, trader, usdGH(100'000)));
env(pay(gatehub, trader, btcGH(100'000)));
env(pay(bitstamp, trader, usdBIT(100'000)));
env.close();
AMM amm{
env,
trader,
usdGH(input.poolUsdGH),
usdBIT(input.poolUsdBIT)};
env.close();
IOUAmount const preSwapLPTokenBalance =
amm.getLPTokensBalance();
env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH)));
env(offer(
trader,
btcGH(input.offer2BtcGH),
usdGH(input.offer2UsdGH)));
env.close();
env(pay(trader, trader, input.sendUsdGH),
path(~usdGH),
path(~btcGH, ~usdGH),
sendmax(input.sendMaxUsdBIT),
txflags(tfPartialPayment));
env.close();
auto const failUsdGH =
features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH;
auto const failUsdBIT =
features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT;
auto const goodUsdGH =
features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH;
auto const goodUsdBIT =
features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT;
auto const lpTokenBalance =
env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt
? *input.lpTokenBalanceAlt
: input.lpTokenBalance;
if (!features[fixAMMOverflowOffer])
{
BEAST_EXPECT(amm.expectBalances(
failUsdGH, failUsdBIT, lpTokenBalance));
}
else
{
BEAST_EXPECT(amm.expectBalances(
goodUsdGH, goodUsdBIT, lpTokenBalance));
// Invariant: LPToken balance must not change in a
// payment or a swap transaction
BEAST_EXPECT(
amm.getLPTokensBalance() == preSwapLPTokenBalance);
// Invariant: The square root of (product of the pool
// balances) must be at least the LPTokenBalance
Number const sqrtPoolProduct =
root2(goodUsdGH * goodUsdBIT);
// Include a tiny tolerance for the test cases using
// .goodUsdGH{usdGH, uint64_t(35'44113971506987),
// -14}, .goodUsdBIT{usdBIT,
// uint64_t(2'821579689703915), -15},
// These two values multiply
// to 99.99999999999994227040383754105 which gets
// internally rounded to 100, due to representation
// error.
BEAST_EXPECT(
(sqrtPoolProduct + Number{1, -14} >=
input.lpTokenBalance));
}
}
}
}
void
testSwapRounding()
{
testcase("swapRounding");
using namespace jtx;
STAmount const xrpPool{XRP, UINT64_C(51600'000981)};
STAmount const iouPool{USD, UINT64_C(803040'9987141784), -10};
STAmount const xrpBob{XRP, UINT64_C(1092'878933)};
STAmount const iouBob{
USD, UINT64_C(3'988035892323031), -28}; // 3.9...e-13
testAMM(
[&](AMM& amm, Env& env) {
// Check our AMM starting conditions.
auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD);
// Set Bob's starting conditions.
env.fund(xrpBob, bob);
env.trust(USD(1'000'000), bob);
env(pay(gw, bob, iouBob));
env.close();
env(offer(bob, XRP(6300), USD(100'000)));
env.close();
// Assert that AMM is unchanged.
BEAST_EXPECT(
amm.expectBalances(xrpBegin, iouBegin, amm.tokens()));
},
{{xrpPool, iouPool}},
889,
std::nullopt,
{jtx::testable_amendments() | fixAMMv1_1});
}
void
testFixAMMOfferBlockedByLOB(FeatureBitset features)
{
testcase("AMM Offer Blocked By LOB");
using namespace jtx;
// Low quality LOB offer blocks AMM liquidity
// USD/XRP crosses AMM
{
Env env(*this, features);
fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
// This offer blocks AMM offer in pre-amendment
env(offer(alice, XRP(1), USD(0.01)));
env.close();
AMM amm(env, gw, XRP(200'000), USD(100'000));
// The offer doesn't cross AMM in pre-amendment code
// It crosses AMM in post-amendment code
env(offer(carol, USD(0.49), XRP(1)));
env.close();
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(amm.expectBalances(
XRP(200'000), USD(100'000), amm.tokens()));
BEAST_EXPECT(expectOffers(
env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
// Carol's offer is blocked by alice's offer
BEAST_EXPECT(expectOffers(
env, carol, 1, {{Amounts{USD(0.49), XRP(1)}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
BEAST_EXPECT(expectOffers(
env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol, 0));
}
}
// There is no blocking offer, the same AMM liquidity is consumed
// pre- and post-amendment.
{
Env env(*this, features);
fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
// There is no blocking offer
// env(offer(alice, XRP(1), USD(0.01)));
AMM amm(env, gw, XRP(200'000), USD(100'000));
// The offer crosses AMM
env(offer(carol, USD(0.49), XRP(1)));
env.close();
// The same result as with the blocking offer
BEAST_EXPECT(amm.expectBalances(
XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
// Carol's offer crosses AMM
BEAST_EXPECT(expectOffers(env, carol, 0));
}
// XRP/USD crosses AMM
{
Env env(*this, features);
fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
// This offer blocks AMM offer in pre-amendment
// It crosses AMM in post-amendment code
env(offer(bob, USD(1), XRPAmount(500)));
env.close();
AMM amm(env, alice, XRP(1'000), USD(500));
env(offer(carol, XRP(100), USD(55)));
env.close();
if (!features[fixAMMv1_1])
{
BEAST_EXPECT(
amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
BEAST_EXPECT(expectOffers(
env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
BEAST_EXPECT(expectOffers(
env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
}
else
{
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
STAmount{USD, UINT64_C(550'000000055), -9},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{{Amounts{
XRPAmount{9'090'909},
STAmount{USD, 4'99999995, -8}}}}));
BEAST_EXPECT(expectOffers(
env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
}
}
// There is no blocking offer, the same AMM liquidity is consumed
// pre- and post-amendment.
{
Env env(*this, features);
fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
AMM amm(env, alice, XRP(1'000), USD(500));
env(offer(carol, XRP(100), USD(55)));
env.close();
BEAST_EXPECT(amm.expectBalances(
XRPAmount(909'090'909),
STAmount{USD, UINT64_C(550'000000055), -9},
amm.tokens()));
BEAST_EXPECT(expectOffers(
env,
carol,
1,
{{Amounts{
XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
}
}
void
testLPTokenBalance(FeatureBitset features)
{
testcase("LPToken Balance");
using namespace jtx;
// Last Liquidity Provider is the issuer of one token
{
std::string logs;
Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
fund(
env,
gw,
{alice, carol},
XRP(1'000'000'000),
{USD(1'000'000'000)});
AMM amm(env, gw, XRP(2), USD(1));
amm.deposit(alice, IOUAmount{1'876123487565916, -15});
amm.deposit(carol, IOUAmount{1'000'000});
amm.withdrawAll(alice);
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0}));
amm.withdrawAll(carol);
BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0}));
auto const lpToken = getAccountLines(
env, gw, amm.lptIssue())[jss::lines][0u][jss::balance];
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
BEAST_EXPECT(
lpToken == "1414.213562373095" &&
lpTokenBalance == "1414.213562373");
if (!features[fixAMMv1_1])
{
amm.withdrawAll(gw, std::nullopt, ter(tecAMM_BALANCE));
BEAST_EXPECT(amm.ammExists());
}
else
{
amm.withdrawAll(gw);
BEAST_EXPECT(!amm.ammExists());
}
}
// Last Liquidity Provider is the issuer of two tokens, or not
// the issuer
for (auto const& lp : {gw, bob})
{
Env env(*this, features);
auto const ABC = gw["ABC"];
fund(
env,
gw,
{alice, carol, bob},
XRP(1'000),
{USD(1'000'000'000), ABC(1'000'000'000'000)});
AMM amm(env, lp, ABC(2'000'000), USD(1));
amm.deposit(alice, IOUAmount{1'876123487565916, -15});
amm.deposit(carol, IOUAmount{1'000'000});
amm.withdrawAll(alice);
amm.withdrawAll(carol);
auto const lpToken = getAccountLines(
env, lp, amm.lptIssue())[jss::lines][0u][jss::balance];
auto const lpTokenBalance =
amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
BEAST_EXPECT(
lpToken == "1414.213562373095" &&
lpTokenBalance == "1414.213562373");
if (!features[fixAMMv1_1])
{
amm.withdrawAll(lp, std::nullopt, ter(tecAMM_BALANCE));
BEAST_EXPECT(amm.ammExists());
}
else
{
amm.withdrawAll(lp);
BEAST_EXPECT(!amm.ammExists());
}
}
// More than one Liquidity Provider
// XRP/IOU
{
Env env(*this, features);
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
AMM amm(env, gw, XRP(10), USD(10));
amm.deposit(alice, 1'000);
auto res =
isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
BEAST_EXPECT(res && !res.value());
res =
isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
BEAST_EXPECT(res && !res.value());
}
// IOU/IOU, issuer of both IOU
{
Env env(*this, features);
fund(env, gw, {alice}, XRP(1'000), {USD(1'000), EUR(1'000)});
AMM amm(env, gw, EUR(10), USD(10));
amm.deposit(alice, 1'000);
auto res =
isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
BEAST_EXPECT(res && !res.value());
res =
isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
BEAST_EXPECT(res && !res.value());
}
// IOU/IOU, issuer of one IOU
{
Env env(*this, features);
Account const gw1("gw1");
auto const YAN = gw1["YAN"];
fund(env, gw, {gw1}, XRP(1'000), {USD(1'000)});
fund(env, gw1, {gw}, XRP(1'000), {YAN(1'000)}, Fund::IOUOnly);
AMM amm(env, gw1, YAN(10), USD(10));
amm.deposit(gw, 1'000);
auto res =
isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
BEAST_EXPECT(res && !res.value());
res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1);
BEAST_EXPECT(res && !res.value());
}
}
void
testAMMClawback(FeatureBitset features)
{
testcase("test clawback from AMM account");
using namespace jtx;
// Issuer has clawback enabled
Env env(*this, features);
env.fund(XRP(1'000), gw);
env(fset(gw, asfAllowTrustLineClawback));
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
env.close();
// If featureAMMClawback is not enabled, AMMCreate is not allowed for
// clawback-enabled issuer
if (!features[featureAMMClawback])
{
AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION));
env(fclear(gw, asfAllowTrustLineClawback));
env.close();
// Can't be cleared
AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
}
// If featureAMMClawback is enabled, AMMCreate is allowed for
// clawback-enabled issuer. Clawback from the AMM Account is not
// allowed, which will return tecAMM_ACCOUNT or tecPSEUDO_ACCOUNT,
// depending on whether SingleAssetVault is enabled. We can only use
// AMMClawback transaction to claw back from AMM Account.
else
{
AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS));
AMM amm1(env, alice, USD(100), XRP(200), ter(tecDUPLICATE));
// Construct the amount being clawed back using AMM account.
// By doing this, we make the clawback transaction's Amount field's
// subfield `issuer` to be the AMM account, which means
// we are clawing back from an AMM account. This should return an
// error because regular Clawback transaction is not
// allowed for clawing back from an AMM account. Please notice the
// `issuer` subfield represents the account being clawed back, which
// is confusing.
auto const error = features[featureSingleAssetVault]
? ter{tecPSEUDO_ACCOUNT}
: ter{tecAMM_ACCOUNT};
Issue usd(USD.issue().currency, amm.ammAccount());
auto amount = amountFromString(usd, "10");
env(claw(gw, amount), error);
}
}
void
testAMMDepositWithFrozenAssets(FeatureBitset features)
{
testcase("test AMMDeposit with frozen assets");
using namespace jtx;
// This lambda function is used to create trustlines
// between gw and alice, and create an AMM account.
// And also test the callback function.
auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm)> cb) {
env.fund(XRP(1'000), gw);
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
env.close();
AMM amm(env, alice, XRP(100), USD(100), ter(tesSUCCESS));
env(trust(gw, alice["USD"](0), tfSetFreeze));
cb(amm);
};
// Deposit two assets, one of which is frozen,
// then we should get tecFROZEN error.
{
Env env(*this, features);
testAMMDeposit(env, [&](AMM& amm) {
amm.deposit(
alice,
USD(100),
XRP(100),
std::nullopt,
tfTwoAsset,
ter(tecFROZEN));
});
}
// Deposit one asset, which is the frozen token,
// then we should get tecFROZEN error.
{
Env env(*this, features);
testAMMDeposit(env, [&](AMM& amm) {
amm.deposit(
alice,
USD(100),
std::nullopt,
std::nullopt,
tfSingleAsset,
ter(tecFROZEN));
});
}
if (features[featureAMMClawback])
{
// Deposit one asset which is not the frozen token,
// but the other asset is frozen. We should get tecFROZEN error
// when feature AMMClawback is enabled.
Env env(*this, features);
testAMMDeposit(env, [&](AMM& amm) {
amm.deposit(
alice,
XRP(100),
std::nullopt,
std::nullopt,
tfSingleAsset,
ter(tecFROZEN));
});
}
else
{
// Deposit one asset which is not the frozen token,
// but the other asset is frozen. We will get tecSUCCESS
// when feature AMMClawback is not enabled.
Env env(*this, features);
testAMMDeposit(env, [&](AMM& amm) {
amm.deposit(
alice,
XRP(100),
std::nullopt,
std::nullopt,
tfSingleAsset,
ter(tesSUCCESS));
});
}
}
void
testFixReserveCheckOnWithdrawal(FeatureBitset features)
{
testcase("Fix Reserve Check On Withdrawal");
using namespace jtx;
auto const err = features[fixAMMv1_2] ? ter(tecINSUFFICIENT_RESERVE)
: ter(tesSUCCESS);
auto test = [&](auto&& cb) {
Env env(*this, features);
auto const starting_xrp =
reserve(env, 2) + env.current()->fees().base * 5;
env.fund(starting_xrp, gw);
env.fund(starting_xrp, alice);
env.trust(USD(2'000), alice);
env.close();
env(pay(gw, alice, USD(2'000)));
env.close();
AMM amm(env, gw, EUR(1'000), USD(1'000));
amm.deposit(alice, USD(1));
cb(amm);
};
// Equal withdraw
test([&](AMM& amm) { amm.withdrawAll(alice, std::nullopt, err); });
// Equal withdraw with a limit
test([&](AMM& amm) {
amm.withdraw(WithdrawArg{
.account = alice,
.asset1Out = EUR(0.1),
.asset2Out = USD(0.1),
.err = err});
amm.withdraw(WithdrawArg{
.account = alice,
.asset1Out = USD(0.1),
.asset2Out = EUR(0.1),
.err = err});
});
// Single withdraw
test([&](AMM& amm) {
amm.withdraw(WithdrawArg{
.account = alice, .asset1Out = EUR(0.1), .err = err});
amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1)});
});
}
void
testFailedPseudoAccount()
{
using namespace test::jtx;
auto const testCase = [&](std::string suffix, FeatureBitset features) {
testcase("Fail pseudo-account allocation " + suffix);
std::string logs;
Env env{*this, features, std::make_unique<CaptureLogs>(&logs)};
env.fund(XRP(30'000), gw, alice);
env.close();
env(trust(alice, gw["USD"](30'000), 0));
env(pay(gw, alice, USD(10'000)));
env.close();
STAmount amount = XRP(10'000);
STAmount amount2 = USD(10'000);
auto const keylet = keylet::amm(amount.issue(), amount2.issue());
for (int i = 0; i < 256; ++i)
{
AccountID const accountId =
ripple::pseudoAccountAddress(*env.current(), keylet.key);
env(pay(env.master.id(), accountId, XRP(1000)),
seq(autofill),
fee(autofill),
sig(autofill));
}
AMM ammAlice(
env,
alice,
amount,
amount2,
features[featureSingleAssetVault] ? ter{terADDRESS_COLLISION}
: ter{tecDUPLICATE});
};
testCase(
"tecDUPLICATE",
testable_amendments() - featureSingleAssetVault -
featureLendingProtocol);
testCase(
"terADDRESS_COLLISION",
testable_amendments() | featureSingleAssetVault);
}
void
testDepositAndWithdrawRounding(FeatureBitset features)
{
testcase("Deposit and Withdraw Rounding V2");
using namespace jtx;
auto const XPM = gw["XPM"];
STAmount xrpBalance{XRPAmount(692'614'492'126)};
STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8};
STAmount amount{XPM, UINT64_C(6'566'496939465400), -12};
std::uint16_t tfee = 941;
auto test = [&](auto&& cb, std::uint16_t tfee_) {
Env env(*this, features);
env.fund(XRP(1'000'000), gw);
env.fund(XRP(1'000), alice);
env(trust(alice, XPM(7'000)));
env(pay(gw, alice, amount));
AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_});
// AMM LPToken balance required to replicate single deposit failure
STAmount lptAMMBalance{
amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6};
auto const burn =
IOUAmount{amm.getLPTokensBalance() - lptAMMBalance};
// burn tokens to get to the required AMM state
env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn}));
cb(amm, env);
};
test(
[&](AMM& amm, Env& env) {
auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS)
: ter(tecUNFUNDED_AMM);
amm.deposit(DepositArg{
.account = alice, .asset1In = amount, .err = err});
},
tfee);
test(
[&](AMM& amm, Env& env) {
auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM);
auto const withdraw = STAmount{XPM, 1, -5};
amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}});
auto const [amount_, amount2_, lptAMM_] =
amm.balances(XRP, XPM);
if (!env.enabled(fixAMMv1_3))
BEAST_EXPECT((amount2 - amount2_) > withdraw);
else
BEAST_EXPECT((amount2 - amount2_) <= withdraw);
},
0);
}
void
invariant(
jtx::AMM& amm,
jtx::Env& env,
std::string const& msg,
bool shouldFail)
{
auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR);
NumberRoundModeGuard g(
env.enabled(fixAMMv1_3) ? Number::upward : Number::getround());
auto const res = root2(amount * amount2);
if (shouldFail)
BEAST_EXPECT(res < lptBalance);
else
BEAST_EXPECT(res >= lptBalance);
}
void
testDepositRounding(FeatureBitset all)
{
testcase("Deposit Rounding");
using namespace jtx;
// Single asset deposit
for (auto const& deposit :
{STAmount(EUR, 1, 1),
STAmount(EUR, 1, 2),
STAmount(EUR, 1, 5),
STAmount(EUR, 1, -3), // fail
STAmount(EUR, 1, -6),
STAmount(EUR, 1, -9)})
{
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
ammAlice.deposit(
DepositArg{.account = bob, .asset1In = deposit});
invariant(
ammAlice,
env,
"dep1",
deposit == STAmount{EUR, 1, -3} &&
!env.enabled(fixAMMv1_3));
},
{{GBP(30'000), EUR(30'000)}},
0,
std::nullopt,
{all});
}
// Two-asset proportional deposit (1:1 pool ratio)
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
STAmount const depositEuro{
EUR, UINT64_C(10'1234567890123456), -16};
STAmount const depositGBP{
GBP, UINT64_C(10'1234567890123456), -16};
ammAlice.deposit(DepositArg{
.account = bob,
.asset1In = depositEuro,
.asset2In = depositGBP});
invariant(ammAlice, env, "dep2", false);
},
{{GBP(30'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// Two-asset proportional deposit (1:3 pool ratio)
for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9})
{
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
STAmount const depositEuro{EUR, 1, exponent};
STAmount const depositGBP{GBP, 1, exponent};
ammAlice.deposit(DepositArg{
.account = bob,
.asset1In = depositEuro,
.asset2In = depositGBP});
invariant(
ammAlice,
env,
"dep3",
exponent != -3 && !env.enabled(fixAMMv1_3));
},
{{GBP(10'000), EUR(30'000)}},
0,
std::nullopt,
{all});
}
// tfLPToken deposit
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
ammAlice.deposit(DepositArg{
.account = bob,
.tokens = IOUAmount{10'1234567890123456, -16}});
invariant(ammAlice, env, "dep4", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfOneAssetLPToken deposit
for (auto const& tokens :
{IOUAmount{1, -3},
IOUAmount{1, -2},
IOUAmount{1, -1},
IOUAmount{1},
IOUAmount{10},
IOUAmount{100},
IOUAmount{1'000},
IOUAmount{10'000}})
{
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(1'000'000)},
Fund::Acct);
env.close();
ammAlice.deposit(DepositArg{
.account = bob,
.tokens = tokens,
.asset1In = STAmount{EUR, 1, 6}});
invariant(ammAlice, env, "dep5", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
}
// Single deposit with EP not exceeding specified:
// 1'000 GBP with EP not to exceed 5 (GBP/TokensOut)
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
ammAlice.deposit(
bob, GBP(1'000), std::nullopt, STAmount{GBP, 5});
invariant(ammAlice, env, "dep6", false);
},
{{GBP(30'000), EUR(30'000)}},
0,
std::nullopt,
{all});
}
void
testWithdrawRounding(FeatureBitset all)
{
testcase("Withdraw Rounding");
using namespace jtx;
// tfLPToken mode
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(alice, 1'000);
invariant(ammAlice, env, "with1", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfWithdrawAll mode
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(
WithdrawArg{.account = alice, .flags = tfWithdrawAll});
invariant(ammAlice, env, "with2", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfTwoAsset withdraw mode
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(WithdrawArg{
.account = alice,
.asset1Out = STAmount{GBP, 3'500},
.asset2Out = STAmount{EUR, 15'000},
.flags = tfTwoAsset});
invariant(ammAlice, env, "with3", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfSingleAsset withdraw mode
// Note: This test fails with 0 trading fees, but doesn't fail if
// trading fees is set to 1'000 -- I suspect the compound operations
// in AMMHelpers.cpp:withdrawByTokens compensate for the rounding
// errors
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(WithdrawArg{
.account = alice,
.asset1Out = STAmount{GBP, 1'234},
.flags = tfSingleAsset});
invariant(ammAlice, env, "with4", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfOneAssetWithdrawAll mode
testAMM(
[&](AMM& ammAlice, Env& env) {
fund(
env,
gw,
{bob},
XRP(10'000'000),
{GBP(100'000), EUR(100'000)},
Fund::Acct);
env.close();
ammAlice.deposit(DepositArg{
.account = bob, .asset1In = STAmount{GBP, 3'456}});
ammAlice.withdraw(WithdrawArg{
.account = bob,
.asset1Out = STAmount{GBP, 1'000},
.flags = tfOneAssetWithdrawAll});
invariant(ammAlice, env, "with5", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfOneAssetLPToken mode
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(WithdrawArg{
.account = alice,
.tokens = 1'000,
.asset1Out = STAmount{GBP, 100},
.flags = tfOneAssetLPToken});
invariant(ammAlice, env, "with6", false);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
// tfLimitLPToken mode
testAMM(
[&](AMM& ammAlice, Env& env) {
ammAlice.withdraw(WithdrawArg{
.account = alice,
.asset1Out = STAmount{GBP, 100},
.maxEP = IOUAmount{2},
.flags = tfLimitLPToken});
invariant(ammAlice, env, "with7", true);
},
{{GBP(7'000), EUR(30'000)}},
0,
std::nullopt,
{all});
}
void
run() override
{
FeatureBitset const all{jtx::testable_amendments()};
FeatureBitset const featuresNoSAV =
all - featureSingleAssetVault - featureLendingProtocol;
testInvalidInstance();
testInstanceCreate();
testInvalidDeposit(all);
testInvalidDeposit(all - featureAMMClawback);
testDeposit();
testInvalidWithdraw();
testWithdraw();
testInvalidFeeVote();
testFeeVote();
testInvalidBid();
testBid(all);
testBid(all - fixAMMv1_3);
testBid(all - fixAMMv1_1 - fixAMMv1_3);
testInvalidAMMPayment();
testBasicPaymentEngine(all);
testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3);
testBasicPaymentEngine(all - fixReducedOffersV2);
testBasicPaymentEngine(
all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2);
testAMMTokens();
testAmendment();
testFlags();
testRippling();
testAMMAndCLOB(all);
testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
testTradingFee(all);
testTradingFee(all - fixAMMv1_3);
testTradingFee(all - fixAMMv1_1 - fixAMMv1_3);
testAdjustedTokens(all);
testAdjustedTokens(all - fixAMMv1_3);
testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3);
testAutoDelete();
testClawback();
testAMMID();
testSelection(all);
testSelection(all - fixAMMv1_1 - fixAMMv1_3);
testFixDefaultInnerObj();
testMalformed();
testFixOverflowOffer(all);
testFixOverflowOffer(all - fixAMMv1_3);
testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
testSwapRounding();
testFixChangeSpotPriceQuality(all);
testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
testFixAMMOfferBlockedByLOB(all);
testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
testLPTokenBalance(all);
testLPTokenBalance(all - fixAMMv1_3);
testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3);
testAMMClawback(all);
testAMMClawback(featuresNoSAV);
testAMMClawback(featuresNoSAV - featureAMMClawback);
testAMMClawback(all - featureAMMClawback);
testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
testAMMDepositWithFrozenAssets(all);
testAMMDepositWithFrozenAssets(all - featureAMMClawback);
testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
testAMMDepositWithFrozenAssets(
all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
testFixReserveCheckOnWithdrawal(all);
testFixReserveCheckOnWithdrawal(all - fixAMMv1_2);
testDepositAndWithdrawRounding(all);
testDepositAndWithdrawRounding(all - fixAMMv1_3);
testDepositRounding(all);
testDepositRounding(all - fixAMMv1_3);
testWithdrawRounding(all);
testWithdrawRounding(all - fixAMMv1_3);
testFailedPseudoAccount();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, ripple, 1);
} // namespace test
} // namespace ripple