mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-30 10:30:22 +00:00
912 lines
33 KiB
C++
912 lines
33 KiB
C++
#include <xrpl/basics/Number.h>
|
|
#include <xrpl/ledger/Sandbox.h>
|
|
#include <xrpl/protocol/AMMCore.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMHelpers.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMUtils.h>
|
|
#include <xrpl/tx/transactors/AMM/AMMWithdraw.h>
|
|
|
|
namespace xrpl {
|
|
|
|
bool
|
|
AMMWithdraw::checkExtraFeatures(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)
|
|
{
|
|
// LCOV_EXCL_START
|
|
JLOG(ctx.j.debug()) << "AMM Withdraw: reserves or tokens balance is zero.";
|
|
return tecINTERNAL;
|
|
// LCOV_EXCL_STOP
|
|
}
|
|
|
|
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 = verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account_); !res)
|
|
return {res.error(), 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, "xrpl::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 xrpl
|