Files
rippled/src/xrpld/app/tx/detail/AMMWithdraw.cpp
Ed Hennis 37745cb5b2 Rename Transactor preflight functions
- Rename Transactor::preflight to invokePreflight.
- Rename doPreflight back to preflight.
- Update instructions.
- With preflight1 & 2 now uncallable, in-flight code in other
  branches should be easier to convert.
2025-07-11 19:17:11 -04:00

1120 lines
36 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 <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/app/tx/detail/AMMWithdraw.h>
#include <xrpld/ledger/Sandbox.h>
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
bool
AMMWithdraw::isEnabled(PreflightContext const& ctx)
{
return ammEnabled(ctx.rules);
}
std::uint32_t
AMMWithdraw::getFlagsMask(PreflightContext const& ctx)
{
return tfWithdrawMask;
}
NotTEC
AMMWithdraw::preflight(PreflightContext const& ctx)
{
auto const flags = ctx.tx.getFlags();
auto const amount = ctx.tx[~sfAmount];
auto const amount2 = ctx.tx[~sfAmount2];
auto const ePrice = ctx.tx[~sfEPrice];
auto const lpTokens = ctx.tx[~sfLPTokenIn];
// Valid combinations are:
// LPTokens
// tfWithdrawAll
// Amount
// tfOneAssetWithdrawAll & Amount
// Amount and Amount2
// Amount and LPTokens
// Amount and EPrice
if (std::popcount(flags & tfWithdrawSubTx) != 1)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid flags.";
return temMALFORMED;
}
if (flags & tfLPToken)
{
if (!lpTokens || amount || amount2 || ePrice)
return temMALFORMED;
}
else if (flags & tfWithdrawAll)
{
if (lpTokens || amount || amount2 || ePrice)
return temMALFORMED;
}
else if (flags & tfOneAssetWithdrawAll)
{
if (!amount || lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if (flags & tfSingleAsset)
{
if (!amount || lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if (flags & tfTwoAsset)
{
if (!amount || !amount2 || lpTokens || ePrice)
return temMALFORMED;
}
else if (flags & tfOneAssetLPToken)
{
if (!amount || !lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if (flags & tfLimitLPToken)
{
if (!amount || !ePrice || lpTokens || amount2)
return temMALFORMED;
}
auto const asset = ctx.tx[sfAsset].get<Issue>();
auto const asset2 = ctx.tx[sfAsset2].get<Issue>();
if (auto const res = invalidAMMAssetPair(asset, asset2))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair.";
return res;
}
if (amount && amount2 && amount->issue() == amount2->issue())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens, same issue."
<< amount->issue() << " " << amount2->issue();
return temBAD_AMM_TOKENS;
}
if (lpTokens && *lpTokens <= beast::zero)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens.";
return temBAD_AMM_TOKENS;
}
if (amount)
{
if (auto const res = invalidAMMAmount(
*amount,
std::make_optional(std::make_pair(asset, asset2)),
(flags & (tfOneAssetWithdrawAll | tfOneAssetLPToken)) ||
ePrice))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid Asset1Out";
return res;
}
}
if (amount2)
{
if (auto const res = invalidAMMAmount(
*amount2, std::make_optional(std::make_pair(asset, asset2))))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid Asset2OutAmount";
return res;
}
}
if (ePrice)
{
if (auto const res = invalidAMMAmount(*ePrice))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice";
return res;
}
}
return tesSUCCESS;
}
static std::optional<STAmount>
tokensWithdraw(
STAmount const& lpTokens,
std::optional<STAmount> const& tokensIn,
std::uint32_t flags)
{
if (flags & (tfWithdrawAll | tfOneAssetWithdrawAll))
return lpTokens;
return tokensIn;
}
TER
AMMWithdraw::preclaim(PreclaimContext const& ctx)
{
auto const accountID = ctx.tx[sfAccount];
auto const ammSle =
ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
if (!ammSle)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair.";
return terNO_AMM;
}
auto const amount = ctx.tx[~sfAmount];
auto const amount2 = ctx.tx[~sfAmount2];
auto const expected = ammHolds(
ctx.view,
*ammSle,
amount ? amount->issue() : std::optional<Issue>{},
amount2 ? amount2->issue() : std::optional<Issue>{},
FreezeHandling::fhIGNORE_FREEZE,
ctx.j);
if (!expected)
return expected.error();
auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
if (lptAMMBalance == beast::zero)
return tecAMM_EMPTY;
if (amountBalance <= beast::zero || amount2Balance <= beast::zero ||
lptAMMBalance < beast::zero)
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: reserves or tokens balance is zero.";
return tecINTERNAL; // LCOV_EXCL_LINE
}
auto const ammAccountID = ammSle->getAccountID(sfAccount);
auto checkAmount = [&](std::optional<STAmount> const& amount,
auto const& balance) -> TER {
if (amount)
{
if (amount > balance)
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: withdrawing more than the balance, "
<< *amount;
return tecAMM_BALANCE;
}
if (auto const ter =
requireAuth(ctx.view, amount->issue(), accountID))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: account is not authorized, "
<< amount->issue();
return ter;
}
// AMM account or currency frozen
if (isFrozen(ctx.view, ammAccountID, amount->issue()))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: AMM account or currency is frozen, "
<< to_string(accountID);
return tecFROZEN;
}
// Account frozen
if (isIndividualFrozen(ctx.view, accountID, amount->issue()))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen, "
<< to_string(accountID) << " "
<< to_string(amount->issue().currency);
return tecFROZEN;
}
}
return tesSUCCESS;
};
if (auto const ter = checkAmount(amount, amountBalance))
return ter;
if (auto const ter = checkAmount(amount2, amount2Balance))
return ter;
auto const lpTokens =
ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
auto const lpTokensWithdraw =
tokensWithdraw(lpTokens, ctx.tx[~sfLPTokenIn], ctx.tx.getFlags());
if (lpTokens <= beast::zero)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: tokens balance is zero.";
return tecAMM_BALANCE;
}
if (lpTokensWithdraw && lpTokensWithdraw->issue() != lpTokens.issue())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid LPTokens.";
return temBAD_AMM_TOKENS;
}
if (lpTokensWithdraw && *lpTokensWithdraw > lpTokens)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens.";
return tecAMM_INVALID_TOKENS;
}
if (auto const ePrice = ctx.tx[~sfEPrice];
ePrice && ePrice->issue() != lpTokens.issue())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice.";
return temBAD_AMM_TOKENS;
}
if (ctx.tx.getFlags() & (tfLPToken | tfWithdrawAll))
{
if (auto const ter = checkAmount(amountBalance, amountBalance))
return ter;
if (auto const ter = checkAmount(amount2Balance, amount2Balance))
return ter;
}
return tesSUCCESS;
}
std::pair<TER, bool>
AMMWithdraw::applyGuts(Sandbox& sb)
{
auto const amount = ctx_.tx[~sfAmount];
auto const amount2 = ctx_.tx[~sfAmount2];
auto const ePrice = ctx_.tx[~sfEPrice];
auto ammSle = sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
if (!ammSle)
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
auto const ammAccountID = (*ammSle)[sfAccount];
auto const accountSle = sb.read(keylet::account(ammAccountID));
if (!accountSle)
return {tecINTERNAL, false}; // LCOV_EXCL_LINE
auto const lpTokens =
ammLPHolds(ctx_.view(), *ammSle, ctx_.tx[sfAccount], ctx_.journal);
auto const lpTokensWithdraw =
tokensWithdraw(lpTokens, ctx_.tx[~sfLPTokenIn], ctx_.tx.getFlags());
// Due to rounding, the LPTokenBalance of the last LP
// might not match the LP's trustline balance
if (sb.rules().enabled(fixAMMv1_1))
{
if (auto const res =
isOnlyLiquidityProvider(sb, lpTokens.issue(), account_);
!res)
return {res.error(), false};
else if (res.value())
{
if (withinRelativeDistance(
lpTokens,
ammSle->getFieldAmount(sfLPTokenBalance),
Number{1, -3}))
{
ammSle->setFieldAmount(sfLPTokenBalance, lpTokens);
sb.update(ammSle);
}
else
{
return {tecAMM_INVALID_TOKENS, false};
}
}
}
auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_);
auto const expected = ammHolds(
sb,
*ammSle,
amount ? amount->issue() : std::optional<Issue>{},
amount2 ? amount2->issue() : std::optional<Issue>{},
FreezeHandling::fhZERO_IF_FROZEN,
ctx_.journal);
if (!expected)
return {expected.error(), false};
auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
auto const subTxType = ctx_.tx.getFlags() & tfWithdrawSubTx;
auto const [result, newLPTokenBalance] =
[&,
&amountBalance = amountBalance,
&amount2Balance = amount2Balance,
&lptAMMBalance = lptAMMBalance]() -> std::pair<TER, STAmount> {
if (subTxType & tfTwoAsset)
return equalWithdrawLimit(
sb,
*ammSle,
ammAccountID,
amountBalance,
amount2Balance,
lptAMMBalance,
*amount,
*amount2,
tfee);
if (subTxType & tfOneAssetLPToken || subTxType & tfOneAssetWithdrawAll)
return singleWithdrawTokens(
sb,
*ammSle,
ammAccountID,
amountBalance,
lptAMMBalance,
*amount,
*lpTokensWithdraw,
tfee);
if (subTxType & tfLimitLPToken)
return singleWithdrawEPrice(
sb,
*ammSle,
ammAccountID,
amountBalance,
lptAMMBalance,
*amount,
*ePrice,
tfee);
if (subTxType & tfSingleAsset)
return singleWithdraw(
sb,
*ammSle,
ammAccountID,
amountBalance,
lptAMMBalance,
*amount,
tfee);
if (subTxType & tfLPToken || subTxType & tfWithdrawAll)
{
return equalWithdrawTokens(
sb,
*ammSle,
ammAccountID,
amountBalance,
amount2Balance,
lptAMMBalance,
lpTokens,
*lpTokensWithdraw,
tfee);
}
// should not happen.
// LCOV_EXCL_START
JLOG(j_.error()) << "AMM Withdraw: invalid options.";
return std::make_pair(tecINTERNAL, STAmount{});
// LCOV_EXCL_STOP
}();
if (result != tesSUCCESS)
return {result, false};
auto const res = deleteAMMAccountIfEmpty(
sb,
ammSle,
newLPTokenBalance,
ctx_.tx[sfAsset].get<Issue>(),
ctx_.tx[sfAsset2].get<Issue>(),
j_);
// LCOV_EXCL_START
if (!res.second)
return {res.first, false};
// LCOV_EXCL_STOP
JLOG(ctx_.journal.trace())
<< "AMM Withdraw: tokens " << to_string(newLPTokenBalance.iou()) << " "
<< to_string(lpTokens.iou()) << " " << to_string(lptAMMBalance.iou());
return {tesSUCCESS, true};
}
TER
AMMWithdraw::doApply()
{
// This is the ledger view that we work against. Transactions are applied
// as we go on processing transactions.
Sandbox sb(&ctx_.view());
auto const result = applyGuts(sb);
if (result.second)
sb.apply(ctx_.rawView());
return result.first;
}
std::pair<TER, STAmount>
AMMWithdraw::withdraw(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& amountWithdraw,
std::optional<STAmount> const& amount2Withdraw,
STAmount const& lpTokensAMMBalance,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee)
{
TER ter;
STAmount newLPTokenBalance;
std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) = withdraw(
view,
ammSle,
ammAccount,
account_,
amountBalance,
amountWithdraw,
amount2Withdraw,
lpTokensAMMBalance,
lpTokensWithdraw,
tfee,
FreezeHandling::fhZERO_IF_FROZEN,
isWithdrawAll(ctx_.tx),
mPriorBalance,
j_);
return {ter, newLPTokenBalance};
}
std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
AMMWithdraw::withdraw(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
AccountID const& account,
STAmount const& amountBalance,
STAmount const& amountWithdraw,
std::optional<STAmount> const& amount2Withdraw,
STAmount const& lpTokensAMMBalance,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee,
FreezeHandling freezeHandling,
WithdrawAll withdrawAll,
XRPAmount const& priorBalance,
beast::Journal const& journal)
{
auto const lpTokens = ammLPHolds(view, ammSle, account, journal);
auto const expected = ammHolds(
view,
ammSle,
amountWithdraw.issue(),
std::nullopt,
freezeHandling,
journal);
// LCOV_EXCL_START
if (!expected)
return {expected.error(), STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
auto const [curBalance, curBalance2, _] = *expected;
(void)_;
auto const
[amountWithdrawActual, amount2WithdrawActual, lpTokensWithdrawActual] =
[&]() -> std::tuple<STAmount, std::optional<STAmount>, STAmount> {
if (withdrawAll == WithdrawAll::No)
return adjustAmountsByLPTokens(
amountBalance,
amountWithdraw,
amount2Withdraw,
lpTokensAMMBalance,
lpTokensWithdraw,
tfee,
IsDeposit::No);
return std::make_tuple(
amountWithdraw, amount2Withdraw, lpTokensWithdraw);
}();
if (lpTokensWithdrawActual <= beast::zero ||
lpTokensWithdrawActual > lpTokens)
{
JLOG(journal.debug())
<< "AMM Withdraw: failed to withdraw, invalid LP tokens: "
<< lpTokensWithdrawActual << " " << lpTokens << " "
<< lpTokensAMMBalance;
return {tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, STAmount{}};
}
// Should not happen since the only LP on last withdraw
// has the balance set to the lp token trustline balance.
if (view.rules().enabled(fixAMMv1_1) &&
lpTokensWithdrawActual > lpTokensAMMBalance)
{
// LCOV_EXCL_START
JLOG(journal.debug())
<< "AMM Withdraw: failed to withdraw, unexpected LP tokens: "
<< lpTokensWithdrawActual << " " << lpTokens << " "
<< lpTokensAMMBalance;
return {tecINTERNAL, STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
}
// Withdrawing one side of the pool
if ((amountWithdrawActual == curBalance &&
amount2WithdrawActual != curBalance2) ||
(amount2WithdrawActual == curBalance2 &&
amountWithdrawActual != curBalance))
{
JLOG(journal.debug())
<< "AMM Withdraw: failed to withdraw one side of the pool "
<< " curBalance: " << curBalance << " " << amountWithdrawActual
<< " lpTokensBalance: " << lpTokensWithdraw << " lptBalance "
<< lpTokensAMMBalance;
return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}};
}
// May happen if withdrawing an amount close to one side of the pool
if (lpTokensWithdrawActual == lpTokensAMMBalance &&
(amountWithdrawActual != curBalance ||
amount2WithdrawActual != curBalance2))
{
JLOG(journal.debug())
<< "AMM Withdraw: failed to withdraw all tokens "
<< " curBalance: " << curBalance << " " << amountWithdrawActual
<< " curBalance2: " << amount2WithdrawActual.value_or(STAmount{0})
<< " lpTokensBalance: " << lpTokensWithdraw << " lptBalance "
<< lpTokensAMMBalance;
return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}};
}
// Withdrawing more than the pool's balance
if (amountWithdrawActual > curBalance ||
amount2WithdrawActual > curBalance2)
{
JLOG(journal.debug())
<< "AMM Withdraw: withdrawing more than the pool's balance "
<< " curBalance: " << curBalance << " " << amountWithdrawActual
<< " curBalance2: " << curBalance2 << " "
<< (amount2WithdrawActual ? *amount2WithdrawActual : STAmount{})
<< " lpTokensBalance: " << lpTokensWithdraw << " lptBalance "
<< lpTokensAMMBalance;
return {tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{}};
}
// Check the reserve in case a trustline has to be created
bool const enabledFixAMMv1_2 = view.rules().enabled(fixAMMv1_2);
auto sufficientReserve = [&](Issue const& issue) -> TER {
if (!enabledFixAMMv1_2 || isXRP(issue))
return tesSUCCESS;
if (!view.exists(keylet::line(account, issue)))
{
auto const sleAccount = view.read(keylet::account(account));
if (!sleAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const balance = (*sleAccount)[sfBalance].xrp();
std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount);
// See also SetTrust::doApply()
XRPAmount const reserve(
(ownerCount < 2) ? XRPAmount(beast::zero)
: view.fees().accountReserve(ownerCount + 1));
if (std::max(priorBalance, balance) < reserve)
return tecINSUFFICIENT_RESERVE;
}
return tesSUCCESS;
};
if (auto const err = sufficientReserve(amountWithdrawActual.issue()))
return {err, STAmount{}, STAmount{}, STAmount{}};
// Withdraw amountWithdraw
auto res = accountSend(
view,
ammAccount,
account,
amountWithdrawActual,
journal,
WaiveTransferFee::Yes);
if (res != tesSUCCESS)
{
// LCOV_EXCL_START
JLOG(journal.debug())
<< "AMM Withdraw: failed to withdraw " << amountWithdrawActual;
return {res, STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
}
// Withdraw amount2Withdraw
if (amount2WithdrawActual)
{
if (auto const err = sufficientReserve(amount2WithdrawActual->issue());
err != tesSUCCESS)
return {err, STAmount{}, STAmount{}, STAmount{}};
res = accountSend(
view,
ammAccount,
account,
*amount2WithdrawActual,
journal,
WaiveTransferFee::Yes);
if (res != tesSUCCESS)
{
// LCOV_EXCL_START
JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw "
<< *amount2WithdrawActual;
return {res, STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
}
}
// Withdraw LP tokens
res = redeemIOU(
view,
account,
lpTokensWithdrawActual,
lpTokensWithdrawActual.issue(),
journal);
if (res != tesSUCCESS)
{
// LCOV_EXCL_START
JLOG(journal.debug()) << "AMM Withdraw: failed to withdraw LPTokens";
return {res, STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
}
return std::make_tuple(
tesSUCCESS,
lpTokensAMMBalance - lpTokensWithdrawActual,
amountWithdrawActual,
amount2WithdrawActual);
}
static STAmount
adjustLPTokensIn(
Rules const& rules,
STAmount const& lptAMMBalance,
STAmount const& lpTokensWithdraw,
WithdrawAll withdrawAll)
{
if (!rules.enabled(fixAMMv1_3) || withdrawAll == WithdrawAll::Yes)
return lpTokensWithdraw;
return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No);
}
/** Proportional withdrawal of pool assets for the amount of LPTokens.
*/
std::pair<TER, STAmount>
AMMWithdraw::equalWithdrawTokens(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& amount2Balance,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee)
{
TER ter;
STAmount newLPTokenBalance;
std::tie(ter, newLPTokenBalance, std::ignore, std::ignore) =
equalWithdrawTokens(
view,
ammSle,
account_,
ammAccount,
amountBalance,
amount2Balance,
lptAMMBalance,
lpTokens,
lpTokensWithdraw,
tfee,
FreezeHandling::fhZERO_IF_FROZEN,
isWithdrawAll(ctx_.tx),
mPriorBalance,
ctx_.journal);
return {ter, newLPTokenBalance};
}
std::pair<TER, bool>
AMMWithdraw::deleteAMMAccountIfEmpty(
Sandbox& sb,
std::shared_ptr<SLE> const ammSle,
STAmount const& lpTokenBalance,
Issue const& issue1,
Issue const& issue2,
beast::Journal const& journal)
{
TER ter;
bool updateBalance = true;
if (lpTokenBalance == beast::zero)
{
ter = deleteAMMAccount(sb, issue1, issue2, journal);
if (ter != tesSUCCESS && ter != tecINCOMPLETE)
return {ter, false}; // LCOV_EXCL_LINE
else
updateBalance = (ter == tecINCOMPLETE);
}
if (updateBalance)
{
ammSle->setFieldAmount(sfLPTokenBalance, lpTokenBalance);
sb.update(ammSle);
}
return {ter, true};
}
/** Proportional withdrawal of pool assets for the amount of LPTokens.
*/
std::tuple<TER, STAmount, STAmount, std::optional<STAmount>>
AMMWithdraw::equalWithdrawTokens(
Sandbox& view,
SLE const& ammSle,
AccountID const account,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& amount2Balance,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee,
FreezeHandling freezeHanding,
WithdrawAll withdrawAll,
XRPAmount const& priorBalance,
beast::Journal const& journal)
{
try
{
// Withdrawing all tokens in the pool
if (lpTokensWithdraw == lptAMMBalance)
{
return withdraw(
view,
ammSle,
ammAccount,
account,
amountBalance,
amountBalance,
amount2Balance,
lptAMMBalance,
lpTokensWithdraw,
tfee,
freezeHanding,
WithdrawAll::Yes,
priorBalance,
journal);
}
auto const tokensAdj = adjustLPTokensIn(
view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero)
return {
tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt};
// the adjusted tokens are factored in
auto const frac = divide(tokensAdj, lptAMMBalance, noIssue());
auto const amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
auto const amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
// LP is making equal withdrawal by tokens but the requested amount
// of LP tokens is likely too small and results in one-sided pool
// withdrawal due to round off. Fail so the user withdraws
// more tokens.
if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero)
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
return withdraw(
view,
ammSle,
ammAccount,
account,
amountBalance,
amountWithdraw,
amount2Withdraw,
lptAMMBalance,
tokensAdj,
tfee,
freezeHanding,
withdrawAll,
priorBalance,
journal);
}
// LCOV_EXCL_START
catch (std::exception const& e)
{
JLOG(journal.error())
<< "AMMWithdraw::equalWithdrawTokens exception " << e.what();
}
return {tecINTERNAL, STAmount{}, STAmount{}, STAmount{}};
// LCOV_EXCL_STOP
}
/** All assets withdrawal with the constraints on the maximum amount
* of each asset that the trader is willing to withdraw.
* a = (t/T) * A (5)
* b = (t/T) * B (6)
* where
* A,B: current pool composition
* T: current balance of outstanding LPTokens
* a: balance of asset A being withdrawn
* b: balance of asset B being withdrawn
* t: balance of LPTokens issued to LP after a successful transaction
* Use equation 5 to compute t, given the amount in Asset1Out. Let this be Z
* Use equation 6 to compute the amount of asset2, given Z. Let
* the computed amount of asset2 be X
* If X <= amount in Asset2Out:
* The amount of asset1 to be withdrawn is the one specified in Asset1Out
* The amount of asset2 to be withdrawn is X
* The amount of LPTokens redeemed is Z
* If X> amount in Asset2Out:
* Use equation 5 to compute t, given the amount in Asset2Out. Let this be Q
* Use equation 6 to compute the amount of asset1, given Q.
* Let the computed amount of asset1 be W
* The amount of asset2 to be withdrawn is the one specified in Asset2Out
* The amount of asset1 to be withdrawn is W
* The amount of LPTokens redeemed is Q
*/
std::pair<TER, STAmount>
AMMWithdraw::equalWithdrawLimit(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& amount2Balance,
STAmount const& lptAMMBalance,
STAmount const& amount,
STAmount const& amount2,
std::uint16_t tfee)
{
auto frac = Number{amount} / amountBalance;
auto amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
auto tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
if (amount2Withdraw <= amount2)
{
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amount,
amount2Withdraw,
lptAMMBalance,
tokensAdj,
tfee);
}
frac = Number{amount2} / amount2Balance;
auto amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
if (!view.rules().enabled(fixAMMv1_3))
{
// LCOV_EXCL_START
XRPL_ASSERT(
amountWithdraw <= amount,
"ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw");
// LCOV_EXCL_STOP
}
else if (amountWithdraw > amount)
return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amountWithdraw,
amount2,
lptAMMBalance,
tokensAdj,
tfee);
}
/** Withdraw single asset equivalent to the amount specified in Asset1Out.
* t = T * (c - sqrt(c**2 - 4*R))/2
* where R = b/B, c = R*fee + 2 - fee
* Use equation 7 to compute the t, given the amount in Asset1Out.
*/
std::pair<TER, STAmount>
AMMWithdraw::singleWithdraw(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& lptAMMBalance,
STAmount const& amount,
std::uint16_t tfee)
{
auto const tokens = adjustLPTokensIn(
view.rules(),
lptAMMBalance,
lpTokensIn(amountBalance, amount, lptAMMBalance, tfee),
isWithdrawAll(ctx_.tx));
if (tokens == beast::zero)
{
if (!view.rules().enabled(fixAMMv1_3))
return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE
else
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
// factor in the adjusted tokens
auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens(
view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amountWithdrawAdj,
std::nullopt,
lptAMMBalance,
tokensAdj,
tfee);
}
/** withdrawal of single asset specified in Asset1Out proportional
* to the share represented by the amount of LPTokens.
* Use equation 8 to compute the amount of asset1, given the redeemed t
* represented by LPTokens. Let this be Y.
* If (amount exists for Asset1Out & Y >= amount in Asset1Out) ||
* (amount field does not exist for Asset1Out):
* The amount of asset out is Y
* The amount of LPTokens redeemed is LPTokens
* Equation 8 solves equation 7 @see singleWithdraw for b.
*/
std::pair<TER, STAmount>
AMMWithdraw::singleWithdrawTokens(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& lptAMMBalance,
STAmount const& amount,
STAmount const& lpTokensWithdraw,
std::uint16_t tfee)
{
auto const tokensAdj = adjustLPTokensIn(
view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx));
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// the adjusted tokens are factored in
auto const amountWithdraw =
ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee);
if (amount == beast::zero || amountWithdraw >= amount)
{
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amountWithdraw,
std::nullopt,
lptAMMBalance,
tokensAdj,
tfee);
}
return {tecAMM_FAILED, STAmount{}};
}
/** Withdraw single asset with two constraints.
* a. amount of asset1 if specified (not 0) in Asset1Out specifies the minimum
* amount of asset1 that the trader is willing to withdraw.
* b. The effective price of asset traded out does not exceed the amount
* specified in EPrice
* The effective price (EP) of a trade is defined as the ratio
* of the tokens the trader sold or swapped in (Token B) and
* the token they got in return or swapped out (Token A).
* EP(B/A) = b/a (III)
* b = B * (t1**2 + t1*(f - 2))/(t1*f - 1) (8)
* where t1 = t/T
* Use equations 8 & III and amount in EPrice to compute the two variables:
* asset in as LPTokens. Let this be X
* asset out as that in Asset1Out. Let this be Y
* If (amount exists for Asset1Out & Y >= amount in Asset1Out) ||
* (amount field does not exist for Asset1Out):
* The amount of assetOut is given by Y
* The amount of LPTokens is given by X
*/
std::pair<TER, STAmount>
AMMWithdraw::singleWithdrawEPrice(
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& lptAMMBalance,
STAmount const& amount,
STAmount const& ePrice,
std::uint16_t tfee)
{
// LPTokens is asset in => E = t / a and formula (8) is:
// a = A*(t1**2 + t1*(f - 2))/(t1*f - 1)
// substitute a as t/E =>
// t/E = A*(t1**2 + t1*(f - 2))/(t1*f - 1), t1=t/T => t = t1*T
// t1*T/E = A*((t/T)**2 + t*(f - 2)/T)/(t*f/T - 1) =>
// T/E = A*(t1 + f-2)/(t1*f - 1) =>
// T*(t1*f - 1) = A*E*(t1 + f - 2) =>
// t1*T*f - T = t1*A*E + A*E*(f - 2) =>
// t1*(T*f - A*E) = T + A*E*(f - 2) =>
// t = T*(T + A*E*(f - 2))/(T*f - A*E)
Number const ae = amountBalance * ePrice;
auto const f = getFee(tfee);
auto tokNoRoundCb = [&] {
return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) /
(lptAMMBalance * f - ae);
};
auto tokProdCb = [&] {
return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae);
};
auto const tokensAdj = getRoundedLPTokens(
view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No);
if (tokensAdj <= beast::zero)
{
if (!view.rules().enabled(fixAMMv1_3))
return {tecAMM_FAILED, STAmount{}};
else
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
auto amtNoRoundCb = [&] { return tokensAdj / ePrice; };
auto amtProdCb = [&] { return tokensAdj / ePrice; };
// the adjusted tokens are factored in
auto const amountWithdraw = getRoundedAsset(
view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No);
if (amount == beast::zero || amountWithdraw >= amount)
{
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amountWithdraw,
std::nullopt,
lptAMMBalance,
tokensAdj,
tfee);
}
return {tecAMM_FAILED, STAmount{}};
}
WithdrawAll
AMMWithdraw::isWithdrawAll(STTx const& tx)
{
if (tx[sfFlags] & (tfWithdrawAll | tfOneAssetWithdrawAll))
return WithdrawAll::Yes;
return WithdrawAll::No;
}
} // namespace ripple