Files
rippled/src/test/app/AMM_test.cpp
Ed Hennis 9920037d13 Review feedback from @dangell7
- Cleaned up some of the `LEDGER_ENTRY` macros by eliding unnecessary
  parameters.
- Define the transaction privileges in one place (InvariantCheck.cpp).
- Give `EscrowFinish` the `mayAuthorizeMPT` privilege.
- Rename the test helper `expectLine` to `expectHolding` since
  it handles both trust lines and MPTs.
- Restructure the ""pseudo-account has 2 pseudo-account fields set"
  invariant test to loop over all defined pseudo-account fields.
- Fix `operator<<` for `PrettyAmount` to handle `MPTIssue`s.
- Add enforcement of the `AccountRootsDeletedClean` invariant if SAV is
  enabled, and clarify the comment for the pseudo-account field check.
- Delete the 100% redundant `ttMPTOKEN_ISSUANCE_SET` check in
  `ValidMPTIssuance`.
2025-08-19 19:52:30 -04:00

7978 lines
315 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2023 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/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});
// 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 = AMMBid::preflight(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 = AMMBid::preflight(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 = AMMBid::preflight(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(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);
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()};
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(all - featureSingleAssetVault);
testAMMClawback(all - featureAMMClawback - featureSingleAssetVault);
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