Files
rippled/src/test/app/AMMCalc_test.cpp
2024-06-20 13:57:16 -05:00

463 lines
16 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 <xrpld/app/misc/AMMHelpers.h>
#include <xrpl/protocol/Quality.h>
#include <boost/regex.hpp>
namespace ripple {
namespace test {
/** AMM Calculator. Uses AMM formulas to simulate the payment engine
* expected results. Assuming the formulas are correct some unit-tests can
* be verified. Currently supported operations are:
* - swapIn, find out given in. in can flow through multiple AMM/Offer steps.
* - swapOut, find in given out. out can flow through multiple AMM/Offer steps.
* - lptokens, find lptokens given pool composition.
* - changespq, change AMM spot price (SP) quality. given AMM and Offer
* find out AMM offer, which changes AMM's SP quality to
* the Offer's quality.
*/
class AMMCalc_test : public beast::unit_test::suite
{
using token_iter = boost::sregex_token_iterator;
using steps = std::vector<std::pair<Amounts, bool>>;
using trates = std::map<std::string, std::uint32_t>;
using swapargs = std::tuple<steps, STAmount, trates, std::uint32_t>;
jtx::Account const gw{jtx::Account("gw")};
token_iter const end_;
std::optional<STAmount>
getAmt(token_iter const& p, bool* delimited = nullptr)
{
using namespace jtx;
if (p == end_)
return STAmount{};
std::string str = *p;
str = boost::regex_replace(str, boost::regex("^(A|O)[(]"), "");
boost::smatch match;
// XXX(val))?
boost::regex rx("^([^(]+)[(]([^)]+)[)]([)])?$");
if (boost::regex_search(str, match, rx))
{
if (delimited)
*delimited = (match[3] != "");
if (match[1] == "XRP")
return XRP(std::stoll(match[2]));
// drops
else if (match[1] == "XRPA")
return XRPAmount{std::stoll(match[2])};
return amountFromString(gw[match[1]], match[2]);
}
return std::nullopt;
}
std::optional<std::tuple<std::string, std::uint32_t, bool>>
getRate(token_iter const& p)
{
if (p == end_)
return std::nullopt;
std::string str = *p;
str = boost::regex_replace(str, boost::regex("^T[(]"), "");
// XXX(rate))?
boost::smatch match;
boost::regex rx("^([^(]+)[(]([^)]+)[)]([)])?$");
if (boost::regex_search(str, match, rx))
{
std::string const currency = match[1];
// input is rate * 100, no fraction
std::uint32_t rate = 10'000'000 * std::stoi(match[2].str());
// true if delimited - )
return {{currency, rate, match[3] != "" ? true : false}};
}
return std::nullopt;
}
std::uint32_t
getFee(token_iter const& p)
{
if (p != end_)
{
std::string const s = *p;
return std::stoll(s);
}
return 0;
}
std::optional<std::pair<Amounts, bool>>
getAmounts(token_iter& p)
{
if (p == end_)
return std::nullopt;
std::string const s = *p;
bool const amm = s[0] == 'O' ? false : true;
auto const a1 = getAmt(p++);
if (!a1 || p == end_)
return std::nullopt;
auto const a2 = getAmt(p++);
if (!a2)
return std::nullopt;
return {{{*a1, *a2}, amm}};
}
std::optional<trates>
getTransferRate(token_iter& p)
{
trates rates{};
if (p == end_)
return rates;
std::string str = *p;
if (str[0] != 'T')
return rates;
// T(USD(rate),GBP(rate), ...)
while (p != end_)
{
if (auto const rate = getRate(p++))
{
auto const [currency, trate, delimited] = *rate;
rates[currency] = trate;
if (delimited)
break;
}
else
return std::nullopt;
}
return rates;
}
std::optional<swapargs>
getSwap(token_iter& p)
{
// pairs of amm pool or offer
steps pairs;
// either amm pool or offer
auto isPair = [](auto const& p) {
std::string const s = *p;
return s[0] == 'A' || s[0] == 'O';
};
// get AMM or offer
while (isPair(p))
{
auto const res = getAmounts(p);
if (!res || p == end_)
return std::nullopt;
pairs.push_back(*res);
}
// swap in/out amount
auto const swap = getAmt(p++);
if (!swap)
return std::nullopt;
// optional transfer rate
auto const rate = getTransferRate(p);
if (!rate)
return std::nullopt;
auto const fee = getFee(p);
return {{pairs, *swap, *rate, fee}};
}
std::string
toString(STAmount const& a)
{
std::stringstream str;
str << a.getText() << "/" << to_string(a.issue().currency);
return str.str();
}
STAmount
mulratio(STAmount const& amt, std::uint32_t a, std::uint32_t b, bool round)
{
if (a == b)
return amt;
if (amt.native())
return toSTAmount(mulRatio(amt.xrp(), a, b, round), amt.issue());
return toSTAmount(mulRatio(amt.iou(), a, b, round), amt.issue());
}
void
swapOut(swapargs const& args)
{
auto const vp = std::get<steps>(args);
STAmount sout = std::get<STAmount>(args);
auto const fee = std::get<std::uint32_t>(args);
auto const rates = std::get<trates>(args);
STAmount resultOut = sout;
STAmount resultIn{};
STAmount sin{};
int limitingStep = vp.size();
STAmount limitStepOut{};
auto trate = [&](auto const& amt) {
auto const currency = to_string(amt.issue().currency);
return rates.find(currency) != rates.end() ? rates.at(currency)
: QUALITY_ONE;
};
// swap out reverse
sin = sout;
for (auto it = vp.rbegin(); it != vp.rend(); ++it)
{
sout = mulratio(sin, trate(sin), QUALITY_ONE, true);
auto const [amts, amm] = *it;
// assume no amm limit
if (amm)
{
sin = swapAssetOut(amts, sout, fee);
}
else if (sout <= amts.out)
{
sin = Quality{amts}.ceil_out(amts, sout).in;
}
// limiting step
else
{
sin = amts.in;
limitingStep = vp.rend() - it - 1;
limitStepOut = amts.out;
if (it == vp.rbegin())
resultOut = amts.out;
}
resultIn = sin;
}
sin = limitStepOut;
// swap in if limiting step
for (int i = limitingStep + 1; i < vp.size(); ++i)
{
auto const [amts, amm] = vp[i];
sin = mulratio(sin, QUALITY_ONE, trate(sin), false);
if (amm)
{
sout = swapAssetIn(amts, sin, fee);
}
// assume there is no limiting step in fwd
else
{
sout = Quality{amts}.ceil_in(amts, sin).out;
}
sin = sout;
resultOut = sout;
}
std::cout << "in: " << toString(resultIn)
<< " out: " << toString(resultOut) << std::endl;
}
void
swapIn(swapargs const& args)
{
auto const vp = std::get<steps>(args);
STAmount sin = std::get<STAmount>(args);
auto const fee = std::get<std::uint32_t>(args);
auto const rates = std::get<trates>(args);
STAmount resultIn = sin;
STAmount resultOut{};
STAmount sout{};
int limitingStep = 0;
STAmount limitStepIn{};
auto trate = [&](auto const& amt) {
auto const currency = to_string(amt.issue().currency);
return rates.find(currency) != rates.end() ? rates.at(currency)
: QUALITY_ONE;
};
// Swap in forward
for (auto it = vp.begin(); it != vp.end(); ++it)
{
auto const [amts, amm] = *it;
sin = mulratio(
sin,
QUALITY_ONE,
trate(sin),
false); // out of the next step
// assume no amm limit
if (amm)
{
sout = swapAssetIn(amts, sin, fee);
}
else if (sin <= amts.in)
{
sout = Quality{amts}.ceil_in(amts, sin).out;
}
// limiting step, requested in is greater than the offer
// pay exactly amts.in, which gets amts.out
else
{
sout = amts.out;
limitingStep = it - vp.begin();
limitStepIn = amts.in;
}
sin = sout;
resultOut = sout;
}
sin = limitStepIn;
// swap out if limiting step
for (int i = limitingStep - 1; i >= 0; --i)
{
sout = mulratio(sin, trate(sin), QUALITY_ONE, false);
auto const [amts, amm] = vp[i];
if (amm)
{
sin = swapAssetOut(amts, sout, fee);
}
// assume there is no limiting step
else
{
sin = Quality{amts}.ceil_out(amts, sout).in;
}
resultIn = sin;
}
resultOut = mulratio(resultOut, QUALITY_ONE, trate(resultOut), true);
std::cout << "in: " << toString(resultIn)
<< " out: " << toString(resultOut) << std::endl;
}
void
run() override
{
using namespace jtx;
auto const a = arg();
boost::regex re(",");
token_iter p(a.begin(), a.end(), re, -1);
// Token is denoted as CUR(xxx), where CUR is the currency code
// and xxx is the amount, for instance: XRP(100) or USD(11.5)
// AMM is denoted as A(CUR1(xxx1),CUR2(xxx2)), for instance:
// A(XRP(1000),USD(1000)), the tokens must be in the order
// poolGets/poolPays
// Offer is denoted as O(CUR1(xxx1),CUR2(xxx2)), for instance:
// O(XRP(100),USD(100)), the tokens must be in the order
// takerPays/takerGets
// Transfer rate is denoted as a comma separated list for each
// currency with the transfer rate, for instance:
// T(USD(175),...,EUR(100)).
// the transfer rate is 100 * rate, with no fraction, for instance:
// 1.75 = 1.75 * 100 = 175
// the transfer rate is optional
// AMM trading fee is an integer in {0,1000}, 1000 represents 1%
// the trading fee is optional
auto const exec = [&]() -> bool {
if (p == end_)
return true;
// Swap in to the steps. Execute steps in forward direction first.
// swapin,A(XRP(1000),USD(1000)),O(USD(10),EUR(10)),XRP(11),
// T(USD(125)),1000
// where
// A(...),O(...) are the payment steps, in this case
// consisting of AMM and Offer.
// XRP(11) is the swapIn value. Note the order of tokens in AMM;
// i.e. poolGets/poolPays.
// T(USD(125) is the transfer rate of 1.25%.
// 1000 is AMM trading fee of 1%, the fee is optional.
if (*p == "swapin")
{
if (auto const swap = getSwap(++p); swap)
{
swapIn(*swap);
return true;
}
}
// Swap out of the steps. Execute steps in reverse direction first.
// swapout,A(USD(1000),XRP(1000)),XRP(10),T(USD(100)),100
// where
// A(...) is the payment step, in this case
// consisting of AMM.
// XRP(10) is the swapOut value. Note the order of tokens in AMM:
// i.e. poolGets/poolPays.
// T(USD(100) is the transfer rate of 1%.
// 100 is AMM trading fee of 0.1%.
else if (*p == "swapout")
{
if (auto const swap = getSwap(++p); swap)
{
swapOut(*swap);
return true;
}
}
// Calculate AMM lptokens
// lptokens,USD(1000),XRP(1000)
// where
// USD(...),XRP(...) is the pool composition
else if (*p == "lptokens")
{
if (auto const pool = getAmounts(++p); pool)
{
Account const amm("amm");
auto const LPT = amm["LPT"];
std::cout
<< to_string(
ammLPTokens(pool->first.in, pool->first.out, LPT)
.iou())
<< std::endl;
return true;
}
}
// Change spot price quality - generates AMM offer such that
// when consumed the updated AMM spot price quality is equal
// to the CLOB offer quality
// changespq,A(XRP(1000),USD(1000)),O(XRP(100),USD(99)),10
// where
// A(...) is AMM
// O(...) is CLOB offer
// 10 is AMM trading fee
else if (*p == "changespq")
{
Env env(*this);
if (auto const pool = getAmounts(++p))
{
if (auto const offer = getAmounts(p))
{
auto const fee = getFee(p);
if (auto const ammOffer = changeSpotPriceQuality(
pool->first,
Quality{offer->first},
fee,
env.current()->rules(),
beast::Journal(beast::Journal::getNullSink()));
ammOffer)
std::cout
<< "amm offer: " << toString(ammOffer->in)
<< " " << toString(ammOffer->out)
<< "\nnew pool: "
<< toString(pool->first.in + ammOffer->in)
<< " "
<< toString(pool->first.out - ammOffer->out)
<< std::endl;
else
std::cout << "can't change the pool's SP quality"
<< std::endl;
return true;
}
}
}
return false;
};
bool res = false;
try
{
res = exec();
}
catch (std::exception const& ex)
{
std::cout << ex.what() << std::endl;
}
BEAST_EXPECT(res);
}
};
BEAST_DEFINE_TESTSUITE_MANUAL(AMMCalc, app, ripple);
} // namespace test
} // namespace ripple