Files
rippled/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp

1165 lines
39 KiB
C++

#include <xrpl/tx/transactors/dex/AMMWithdraw.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/helpers/AMMHelpers.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <algorithm>
#include <bit>
#include <cstdint>
#include <exception>
#include <memory>
#include <optional>
#include <tuple>
#include <utility>
namespace xrpl {
bool
AMMWithdraw::checkExtraFeatures(PreflightContext const& ctx)
{
if (!ammEnabled(ctx.rules))
return false;
auto const amount = ctx.tx[~sfAmount];
auto const amount2 = ctx.tx[~sfAmount2];
return ctx.rules.enabled(featureMPTokensV2) ||
(!ctx.tx[sfAsset].holds<MPTIssue>() && !ctx.tx[sfAsset2].holds<MPTIssue>() &&
!(amount && amount->holds<MPTIssue>()) && !(amount2 && amount2->holds<MPTIssue>()));
}
std::uint32_t
AMMWithdraw::getFlagsMask(PreflightContext const& ctx)
{
return tfAMMWithdrawMask;
}
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) != 0u)
{
if (!lpTokens || amount || amount2 || ePrice)
return temMALFORMED;
}
else if ((flags & tfWithdrawAll) != 0u)
{
if (lpTokens || amount || amount2 || ePrice)
return temMALFORMED;
}
else if ((flags & tfOneAssetWithdrawAll) != 0u)
{
if (!amount || lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if ((flags & tfSingleAsset) != 0u)
{
if (!amount || lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if ((flags & tfTwoAsset) != 0u)
{
if (!amount || !amount2 || lpTokens || ePrice)
return temMALFORMED;
}
else if ((flags & tfOneAssetLPToken) != 0u)
{
if (!amount || !lpTokens || amount2 || ePrice)
return temMALFORMED;
}
else if ((flags & tfLimitLPToken) != 0u)
{
if (!amount || !ePrice || lpTokens || amount2)
return temMALFORMED;
}
auto const asset = ctx.tx[sfAsset];
auto const asset2 = ctx.tx[sfAsset2];
if (auto const res = invalidAMMAssetPair(asset, asset2))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid asset pair.";
return res;
}
if (amount && amount2 && amount->asset() == amount2->asset())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens, same issue." << amount->asset() << " "
<< amount2->asset();
return temBAD_AMM_TOKENS;
}
if (lpTokens && *lpTokens <= beast::kZERO)
{
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)) != 0u) || 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)) != 0u)
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->asset() : std::optional<Asset>{},
amount2 ? amount2->asset() : std::optional<Asset>{},
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
ctx.j);
if (!expected)
return expected.error();
auto const [amountBalance, amount2Balance, lptAMMBalance] = *expected;
if (lptAMMBalance == beast::kZERO)
return tecAMM_EMPTY;
if (amountBalance <= beast::kZERO || amount2Balance <= beast::kZERO ||
lptAMMBalance < beast::kZERO)
{
// 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;
}
// WeakAuth - MPToken is created if it doesn't exist.
if (auto const ter =
requireAuth(ctx.view, amount->asset(), accountID, AuthType::WeakAuth))
{
JLOG(ctx.j.debug())
<< "AMM Withdraw: account is not authorized, " << amount->asset();
return ter;
}
// AMM account or currency frozen
if (isFrozen(ctx.view, ammAccountID, amount->asset()))
{
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->asset()))
{
JLOG(ctx.j.debug()) << "AMM Withdraw: account is frozen, " << to_string(accountID)
<< " " << to_string(amount->asset());
return tecFROZEN;
}
if (auto const ter =
checkMPTTxAllowed(ctx.view, ttAMM_WITHDRAW, amount->asset(), accountID);
!isTesSuccess(ter))
return ter;
}
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::kZERO)
{
JLOG(ctx.j.debug()) << "AMM Withdraw: tokens balance is zero.";
return tecAMM_BALANCE;
}
if (lpTokensWithdraw && lpTokensWithdraw->asset() != lpTokens.asset())
{
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->asset() != lpTokens.asset())
{
JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice.";
return temBAD_AMM_TOKENS;
}
if ((ctx.tx.getFlags() & (tfLPToken | tfWithdrawAll)) != 0u)
{
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->asset() : std::optional<Asset>{},
amount2 ? amount2->asset() : std::optional<Asset>{},
FreezeHandling::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
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 (!isTesSuccess(result))
return {result, false};
auto const res = deleteAMMAccountIfEmpty(
sb, ammSle, newLPTokenBalance, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], 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::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
isWithdrawAll(ctx_.tx),
preFeeBalance_,
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,
AuthHandling authHandling,
WithdrawAll withdrawAll,
XRPAmount const& priorBalance,
beast::Journal const& journal)
{
auto const lpTokens = ammLPHolds(view, ammSle, account, journal);
auto const expected = ammHolds(
view, ammSle, amountWithdraw.asset(), std::nullopt, freezeHandling, authHandling, 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::kZERO || 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{}};
}
// Updated pool state must be valid - either all balances are zero
// or all balances are non-zero.
if (view.rules().enabled(featureMPTokensV2))
{
bool const newBalanceZero = (curBalance - amountWithdrawActual) == beast::kZERO;
bool const newBalance2Zero =
(curBalance2 - amount2WithdrawActual.value_or(curBalance2.asset())) == beast::kZERO;
bool const newLPTokensZero = (lpTokensAMMBalance - lpTokensWithdrawActual) == beast::kZERO;
// newBalance2Zero can be zero if that side of the pool is frozen.
// ignore newBalance2Zero if one-sided withdrawal.
bool const valid = [&]() {
if (!amount2WithdrawActual)
return newBalanceZero == newLPTokensZero;
return newBalanceZero == newBalance2Zero && newBalance2Zero == newLPTokensZero;
}();
if (!valid)
{
JLOG(journal.debug()) << "AMM Withdraw: some balances are zero"
<< " 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 or MPT has to be created
bool const enabledFixAmMv12 = view.rules().enabled(fixAMMv1_2);
// If seated after a call to sufficientReserve() then MPToken must be
// authorized
std::optional<Keylet> mptokenKey;
auto sufficientReserve = [&](Asset const& asset) -> TER {
mptokenKey = std::nullopt;
if (!enabledFixAmMv12 || isXRP(asset))
return tesSUCCESS;
bool const isIssue = asset.holds<Issue>();
bool const assetNotExists = [&] {
if (isIssue)
return !view.exists(keylet::line(account, asset.get<Issue>()));
auto const issuanceKey = keylet::mptIssuance(asset.get<MPTIssue>());
mptokenKey = keylet::mptoken(issuanceKey.key, account);
if (!view.exists(*mptokenKey))
return true;
mptokenKey = std::nullopt;
return false;
}();
if (assetNotExists)
{
auto sleAccount = view.peek(keylet::account(account));
if (!sleAccount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const balance = (*sleAccount)[sfBalance];
std::uint32_t const ownerCount = sleAccount->at(sfOwnerCount);
// See also TrustSet::doApply() and MPTokenAuthorize::authorize()
XRPAmount const reserve(
(ownerCount < 2) ? XRPAmount(beast::kZERO)
: view.fees().accountReserve(ownerCount + 1));
auto const balanceAdj = isIssue ? std::max(priorBalance, balance.xrp()) : priorBalance;
if (balanceAdj < reserve)
return tecINSUFFICIENT_RESERVE;
// Update owner count.
if (!isIssue)
adjustOwnerCount(view, sleAccount, 1, journal);
}
return tesSUCCESS;
};
// Create MPToken if it doesn't exist
auto createMPToken = [&](Asset const& asset) -> TER {
// If mptoken is seated then must authorize
if (mptokenKey && account != asset.getIssuer())
{
auto const& mptIssue = asset.get<MPTIssue>();
if (auto const err = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(err))
return err;
if (auto const err = checkCreateMPT(view, mptIssue, account, journal);
!isTesSuccess(err))
{
return err;
}
}
return tesSUCCESS;
};
if (auto const err = sufficientReserve(amountWithdrawActual.asset()))
return {err, STAmount{}, STAmount{}, STAmount{}};
if (auto const res = createMPToken(amountWithdrawActual.asset()); !isTesSuccess(res))
return {res, STAmount{}, STAmount{}, STAmount{}};
// Withdraw amountWithdraw
auto res = accountSend(
view, ammAccount, account, amountWithdrawActual, journal, WaiveTransferFee::Yes);
if (!isTesSuccess(res))
{
// 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->asset()); !isTesSuccess(err))
return {err, STAmount{}, STAmount{}, STAmount{}};
if (auto const res = createMPToken(amount2WithdrawActual->asset()); !isTesSuccess(res))
return {res, STAmount{}, STAmount{}, STAmount{}};
res = accountSend(
view, ammAccount, account, *amount2WithdrawActual, journal, WaiveTransferFee::Yes);
if (!isTesSuccess(res))
{
// 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.get<Issue>(), journal);
if (!isTesSuccess(res))
{
// 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::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
isWithdrawAll(ctx_.tx),
preFeeBalance_,
ctx_.journal);
return {ter, newLPTokenBalance};
}
std::pair<TER, bool>
AMMWithdraw::deleteAMMAccountIfEmpty(
Sandbox& sb,
std::shared_ptr<SLE> const ammSle,
STAmount const& lpTokenBalance,
Asset const& asset1,
Asset const& asset2,
beast::Journal const& journal)
{
TER ter;
bool updateBalance = true;
if (lpTokenBalance == beast::kZERO)
{
ter = deleteAMMAccount(sb, asset1, asset2, journal);
if (!isTesSuccess(ter) && ter != tecINCOMPLETE)
return {ter, false}; // LCOV_EXCL_LINE
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 freezeHandling,
AuthHandling authHandling,
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,
freezeHandling,
authHandling,
WithdrawAll::Yes,
priorBalance,
journal);
}
auto const tokensAdj =
adjustLPTokensIn(view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::kZERO)
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::kZERO || amount2Withdraw == beast::kZERO)
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
return withdraw(
view,
ammSle,
ammAccount,
account,
amountBalance,
amountWithdraw,
amount2Withdraw,
lptAMMBalance,
tokensAdj,
tfee,
freezeHandling,
authHandling,
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 tokensAdj = getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No);
if (view.rules().enabled(fixAMMv1_3) && tokensAdj == beast::kZERO)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
auto const 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::kZERO)
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::kZERO)
{
if (!view.rules().enabled(fixAMMv1_3))
{
return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE
}
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::kZERO)
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::kZERO)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// the adjusted tokens are factored in
auto const amountWithdraw = ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee);
if (amount == beast::kZERO || 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::kZERO)
{
if (!view.rules().enabled(fixAMMv1_3))
{
return {tecAMM_FAILED, STAmount{}};
}
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::kZERO || 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)) != 0u)
return WithdrawAll::Yes;
return WithdrawAll::No;
}
void
AMMWithdraw::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
// No transaction-specific invariants yet (future work).
}
bool
AMMWithdraw::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl