diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 201a579d161..66caa5577c0 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -81,6 +81,7 @@ target_sources (xrpl_core PRIVATE src/ripple/protocol/impl/LedgerFormats.cpp src/ripple/protocol/impl/PublicKey.cpp src/ripple/protocol/impl/Quality.cpp + src/ripple/protocol/impl/QualityFunction.cpp src/ripple/protocol/impl/Rate2.cpp src/ripple/protocol/impl/Rules.cpp src/ripple/protocol/impl/SField.cpp @@ -212,6 +213,7 @@ install ( src/ripple/protocol/Protocol.h src/ripple/protocol/PublicKey.h src/ripple/protocol/Quality.h + src/ripple/protocol/QualityFunction.h src/ripple/protocol/Rate.h src/ripple/protocol/Rules.h src/ripple/protocol/SField.h @@ -376,6 +378,8 @@ target_sources (rippled PRIVATE src/ripple/app/reporting/ReportingETL.cpp src/ripple/app/reporting/ETLSource.cpp src/ripple/app/reporting/P2pProxy.cpp + src/ripple/app/misc/impl/AMM.cpp + src/ripple/app/misc/impl/AMM_formulae.cpp src/ripple/app/misc/CanonicalTXSet.cpp src/ripple/app/misc/FeeVoteImpl.cpp src/ripple/app/misc/HashRouter.cpp @@ -401,6 +405,7 @@ target_sources (rippled PRIVATE src/ripple/app/paths/RippleCalc.cpp src/ripple/app/paths/RippleLineCache.cpp src/ripple/app/paths/TrustLine.cpp + src/ripple/app/paths/impl/AMMLiquidity.cpp src/ripple/app/paths/impl/BookStep.cpp src/ripple/app/paths/impl/DirectStep.cpp src/ripple/app/paths/impl/PaySteps.cpp @@ -417,6 +422,11 @@ target_sources (rippled PRIVATE src/ripple/app/rdb/impl/UnitaryShard.cpp src/ripple/app/rdb/impl/Vacuum.cpp src/ripple/app/rdb/impl/Wallet.cpp + src/ripple/app/tx/impl/AMMBid.cpp + src/ripple/app/tx/impl/AMMCreate.cpp + src/ripple/app/tx/impl/AMMDeposit.cpp + src/ripple/app/tx/impl/AMMVote.cpp + src/ripple/app/tx/impl/AMMWithdraw.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp @@ -582,6 +592,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/AccountObjects.cpp src/ripple/rpc/handlers/AccountOffers.cpp src/ripple/rpc/handlers/AccountTx.cpp + src/ripple/rpc/handlers/AMMInfo.cpp src/ripple/rpc/handlers/BlackList.cpp src/ripple/rpc/handlers/BookOffers.cpp src/ripple/rpc/handlers/CanDelete.cpp @@ -686,6 +697,7 @@ if (tests) src/test/app/AccountDelete_test.cpp src/test/app/AccountTxPaging_test.cpp src/test/app/AmendmentTable_test.cpp + src/test/app/AMM_test.cpp src/test/app/Check_test.cpp src/test/app/CrossingLimits_test.cpp src/test/app/DeliverMin_test.cpp @@ -825,6 +837,7 @@ if (tests) src/test/jtx/Env_test.cpp src/test/jtx/WSClient_test.cpp src/test/jtx/impl/Account.cpp + src/test/jtx/impl/AMM.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp src/test/jtx/impl/ManualTimeKeeper.cpp @@ -943,6 +956,7 @@ if (tests) src/test/rpc/AccountSet_test.cpp src/test/rpc/AccountTx_test.cpp src/test/rpc/AmendmentBlocked_test.cpp + src/test/rpc/AMMInfo_test.cpp src/test/rpc/Book_test.cpp src/test/rpc/DepositAuthorized_test.cpp src/test/rpc/DeliveredAmount_test.cpp diff --git a/Builds/levelization/results/loops.txt b/Builds/levelization/results/loops.txt index cb137f497cb..b025a16ce6b 100644 --- a/Builds/levelization/results/loops.txt +++ b/Builds/levelization/results/loops.txt @@ -43,6 +43,9 @@ Loop: ripple.nodestore ripple.overlay Loop: ripple.overlay ripple.rpc ripple.rpc ~= ripple.overlay +Loop: test.jtx test.rpc + test.rpc > test.jtx + Loop: test.jtx test.toplevel test.toplevel > test.jtx diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index ed6b4e57c3e..2ed8c756a99 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -143,6 +143,7 @@ test.jtx > ripple.json test.jtx > ripple.ledger test.jtx > ripple.net test.jtx > ripple.protocol +test.jtx > ripple.rpc test.jtx > ripple.server test.ledger > ripple.app test.ledger > ripple.basics @@ -204,7 +205,6 @@ test.rpc > ripple.overlay test.rpc > ripple.protocol test.rpc > ripple.resource test.rpc > ripple.rpc -test.rpc > test.jtx test.rpc > test.nodestore test.rpc > test.toplevel test.server > ripple.app diff --git a/src/ripple/app/misc/AMM.h b/src/ripple/app/misc/AMM.h new file mode 100644 index 00000000000..85c0cd41bb3 --- /dev/null +++ b/src/ripple/app/misc/AMM.h @@ -0,0 +1,160 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== +#ifndef RIPPLE_APP_MISC_AMM_H_INLCUDED +#define RIPPLE_APP_MISC_AMM_H_INLCUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class ReadView; +class ApplyView; +class Sandbox; +class STLedgerEntry; +class NetClock; +class STObject; +class Rules; + +/** Calculate AMM account ID. + */ +template +AccountID +calcAccountID(Args const&... args) +{ + ripesha_hasher rsh; + auto hash = sha512Half(args...); + rsh(hash.data(), hash.size()); + return AccountID{static_cast(rsh)}; +} + +/** Calculate AMM group hash. The ltAMM object + * contains all AMM's for the same issues. + */ +uint256 +calcAMMGroupHash(Issue const& issue1, Issue const& issue2); + +/** Calculate Liquidity Provider Token (LPT) Currency. + */ +Currency +calcLPTCurrency(AccountID const& ammAccountID); + +/** Calculate LPT Issue. + */ +Issue +calcLPTIssue(AccountID const& ammAccountID); + +/** Get AMM pool balances. + */ +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + beast::Journal const j); + +/** Get AMM pool and LP token balances. If both optIssue are + * provided then they are used as the AMM token pair issues. + * Otherwise the missing issues are fetched from ammSle. + */ +std::tuple +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + beast::Journal const j); + +/** Get the balance of LP tokens. + */ +STAmount +lpHolds( + ReadView const& view, + AccountID const& ammAccountID, + AccountID const& lpAccount, + beast::Journal const j); + +/** Validate the amount. + * If zero is false and amount is beast::zero then invalid amount. + * Return error code if invalid amount. + */ +std::optional +invalidAmount(std::optional const& a, bool zero = false); + +/** Check if the line is frozen from the issuer. + */ +bool +isFrozen(ReadView const& view, std::optional const& a); + +/** Get AMM SLE and verify that the AMM account exists. + * Return null if SLE not found or AMM account doesn't exist. + */ +std::shared_ptr +getAMMSle(ReadView const& view, uint256 ammID); + +std::shared_ptr +getAMMSle(Sandbox& view, uint256 ammID); + +/** Check if the account requires authorization. + * Return true if issuer's account, account, and trust line exist + * and the account requires authorization. + */ +bool +requireAuth(ReadView const& view, Issue const& issue, AccountID const& account); + +/** Get AMM trading fee for the given account. The fee is discounted + * if the account is the auction slot owner or one of the slot's authorized + * accounts. + */ +std::uint16_t +getTradingFee(SLE const& ammSle, AccountID const& account); + +/** Get Issue from sfToken1/sfToken2 fields. + */ +std::pair +getTokensIssue(SLE const& ammSle); + +/** Send w/o fees. Either from or to must be AMM account. + */ +TER +ammSend( + ApplyView& view, + AccountID const& from, + AccountID const& to, + STAmount const& amount, + beast::Journal j); + +/** Get time slot of the auction slot. + */ +std::uint16_t +timeSlot(NetClock::time_point const& clock, STObject const& auctionSlot); + +bool +ammRequiredAmendments(Rules const&); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMM_H_INLCUDED diff --git a/src/ripple/app/misc/AMM_formulae.h b/src/ripple/app/misc/AMM_formulae.h new file mode 100644 index 00000000000..4ff35be7e45 --- /dev/null +++ b/src/ripple/app/misc/AMM_formulae.h @@ -0,0 +1,253 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_AMM_FORMULAE_H_INCLUDED +#define RIPPLE_APP_MISC_AMM_FORMULAE_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +/** When converting Number to XRP as the result of swap in/out + * operation, round downward/upward respectively to maintain + * the invariant - the new pool product is greater or equal + * to the previous pool product. + */ +inline STAmount +toSTAmount( + Issue const& issue, + Number const& n, + Number::rounding_mode mode = Number::rounding_mode::to_nearest) +{ + if (isXRP(issue)) + { + Number::setround(mode); + auto const res = STAmount{issue, (std::int64_t)n}; + Number::setround(Number::rounding_mode::to_nearest); + return res; + } + return STAmount{issue, n.mantissa(), n.exponent()}; +} + +template +STAmount +toSTAmount(A const& a, Issue const& issue) +{ + if constexpr (std::is_same_v) + return toSTAmount(a, issue); + else if constexpr (std::is_same_v) + return toSTAmount(a); + else + return a; +} + +/** Calculate LP Tokens given AMM pool reserves. + * @param asset1 AMM one side of the pool reserve + * @param asset2 AMM another side of the pool reserve + * @return LP Tokens as IOU + */ +STAmount +calcAMMLPT( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue); + +/** Convert to the fee from the basis points + * @param tfee trading fee in basis points + */ +inline Number +getFee(std::uint16_t tfee) +{ + return Number{tfee} / Number{100000}; +} + +/** Get fee multiplier (1 - tfee) + * @tfee trading fee in basis points + */ +inline Number +feeMult(std::uint16_t tfee) +{ + return 1 - getFee(tfee); +} + +/** Get fee multiplier (1 - tfee / 2) + * @tfee trading fee in basis points + */ +inline Number +feeMultHalf(std::uint16_t tfee) +{ + return 1 - getFee(tfee) / 2; +} + +/** Calculate LP Tokens given asset's deposit amount. + * @param asset1Balance current AMM asset1 balance + * @param asset1Deposit requested asset1 deposit amount + * @param lpTokensBalance LP Tokens balance + * @param tfee trading fee in basis points + * @return tokens + */ +STAmount +calcLPTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lpTokensBalance, + std::uint16_t tfee); + +/** Calculate asset deposit given LP Tokens. + * @param asset1Balance current AMM asset1 balance + * @param lpTokensBalance LP Tokens balance + * @param ammTokensBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return + */ +STAmount +calcAssetIn( + STAmount const& asset1Balance, + STAmount const& lpTokensBalance, + STAmount const& ammTokensBalance, + std::uint16_t tfee); + +/** Calculate LP Tokens given asset's withdraw amount. Return 0 + * if can't calculate. + * @param asset1Balance current AMM asset1 balance + * @param asset1Withdraw requested asset1 withdraw amount + * @param lpTokensBalance LP Tokens balance + * @param tfee trading fee in basis points + * @return tokens out amount + */ +STAmount +calcLPTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lpTokensBalance, + std::uint16_t tfee); + +/** Calculate asset withdrawal by tokens + * @param assetBalance balance of the asset being withdrawn + * @param lptAMMBalance total AMM Tokens balance + * @param lpTokens LP Tokens balance + * @param tfee trading fee in basis points + * @return calculated asset amount + */ +STAmount +calcWithdrawalByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint32_t tfee); + +/** Calculate AMM's Spot Price + * @param asset1Balance current AMM asset1 balance + * @param asset2Balance current AMM asset2 balance + * @param tfee trading fee in basis points + * @return spot price + */ +STAmount +calcSpotPrice( + STAmount const& asset1Balance, + STAmount const& asset2Balance, + std::uint16_t tfee); + +/** Get asset2 amount based on new AMM's Spot Price. + * @param asset1Balance current AMM asset1 balance + * @param asset2Balance current AMM asset2 balance + * @param newSP requested SP of asset1 relative to asset2 + * @param tfee trading fee in basis points + * @return + */ +std::optional +changeSpotPrice( + STAmount const& assetInBalance, + STAmount const& assetOuBalance, + STAmount const& newSP, + std::uint16_t tfee); + +/** Find in/out amounts to change the spot price quality to the requested + * quality. + * @param pool AMM pool balances + * @param quality requested quality + * @param tfee trading fee in basis points + * @return seated in/out amounts if the quality can be changed + */ +std::optional +changeSpotPriceQuality( + Amounts const& pool, + Quality const& quality, + std::uint32_t tfee); + +/** Swap assetIn into the pool and swap out a proportional amount + * of the other asset. + * @param pool current AMM pool balances + * @param assetIn amount to swap in + * @param tfee trading fee in basis points + * @return + */ +template +STAmount +swapAssetIn(Amounts const& pool, TIn const& assetIn, std::uint16_t tfee) +{ + auto const res = toSTAmount( + pool.out.issue(), + pool.out * (1 - pool.in / (pool.in + assetIn * feeMult(tfee))), + Number::rounding_mode::downward); + return res; +} + +/** Swap assetOut out of the pool and swap in a proportional amount + * of the other asset. + * @param pool current AMM pool balances + * @param assetOut amount to swap out + * @param tfee trading fee in basis points + * @return + */ +template +STAmount +swapAssetOut(Amounts const& pool, TOut const& assetOut, std::uint16_t tfee) +{ + auto const res = toSTAmount( + pool.in.issue(), + pool.in * (pool.out / (pool.out - assetOut) - 1) / feeMult(tfee), + Number::rounding_mode::upward); + return res; +} + +/** Get T amount + */ +template +T +get(STAmount const& a) +{ + if constexpr (std::is_same_v) + return a.iou(); + else if constexpr (std::is_same_v) + return a.xrp(); + else + return a; +} + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMM_FORMULAE_H_INCLUDED diff --git a/src/ripple/app/misc/impl/AMM.cpp b/src/ripple/app/misc/impl/AMM.cpp new file mode 100644 index 00000000000..dd9a14a189b --- /dev/null +++ b/src/ripple/app/misc/impl/AMM.cpp @@ -0,0 +1,275 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include + +namespace ripple { + +uint256 +calcAMMGroupHash(Issue const& issue1, Issue const& issue2) +{ + if (issue1 < issue2) + return sha512Half( + issue1.account, issue1.currency, issue2.account, issue2.currency); + return sha512Half( + issue2.account, issue2.currency, issue1.account, issue1.currency); +} + +Currency +calcLPTCurrency(AccountID const& ammAccountID) +{ + return Currency::fromVoid(ammAccountID.data()); +} + +Issue +calcLPTIssue(AccountID const& ammAccountID) +{ + return Issue(calcLPTCurrency(ammAccountID), ammAccountID); +} + +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + beast::Journal const j) +{ + auto const assetInBalance = accountHolds( + view, + ammAccountID, + issue1.currency, + issue1.account, + FreezeHandling::fhZERO_IF_FROZEN, + j); + auto const assetOutBalance = accountHolds( + view, + ammAccountID, + issue2.currency, + issue2.account, + FreezeHandling::fhZERO_IF_FROZEN, + j); + return std::make_pair(assetInBalance, assetOutBalance); +} + +std::tuple +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + beast::Journal const j) +{ + auto const [issue1, issue2] = [&]() -> std::pair { + if (optIssue1 && optIssue2) + return {*optIssue1, *optIssue2}; + auto const [issue1, issue2] = getTokensIssue(ammSle); + if (optIssue1) + { + if (*optIssue1 == issue1) + return {issue1, issue2}; + else if (*optIssue1 == issue2) + return {issue2, issue1}; + Throw("ammHolds: Invalid optIssue1."); + } + else if (optIssue2) + { + if (*optIssue2 == issue2) + return {issue2, issue1}; + else if (*optIssue2 == issue1) + return {issue1, issue2}; + Throw("ammHolds: Invalid optIssue2."); + } + return {issue1, issue2}; + }(); + auto const [asset1, asset2] = ammPoolHolds( + view, ammSle.getAccountID(sfAMMAccount), issue1, issue2, j); + return {asset1, asset2, ammSle.getFieldAmount(sfLPTokenBalance)}; +} + +STAmount +lpHolds( + ReadView const& view, + AccountID const& ammAccountID, + AccountID const& lpAccount, + beast::Journal const j) +{ + auto const lptIssue = calcLPTIssue(ammAccountID); + return accountHolds( + view, + lpAccount, + lptIssue.currency, + lptIssue.account, + FreezeHandling::fhZERO_IF_FROZEN, + j); +} + +std::optional +invalidAmount(std::optional const& a, bool zero) +{ + if (!a) + return std::nullopt; + if (badCurrency() == a->getCurrency()) + return temBAD_CURRENCY; + if (a->native() && a->native() != !a->getIssuer()) + return temBAD_ISSUER; + if (!zero && *a <= beast::zero) + return temBAD_AMOUNT; + return std::nullopt; +} + +bool +isFrozen(ReadView const& view, std::optional const& a) +{ + return a && !a->native() && isGlobalFrozen(view, a->getIssuer()); +} + +std::shared_ptr +getAMMSle(ReadView const& view, uint256 ammID) +{ + if (auto const sle = view.read(keylet::amm(ammID)); + (!sle || !view.read(keylet::account(sle->getAccountID(sfAMMAccount))))) + return nullptr; + else + return sle; +} + +std::shared_ptr +getAMMSle(Sandbox& view, uint256 ammID) +{ + if (auto const sle = view.peek(keylet::amm(ammID)); + (!sle || !view.read(keylet::account(sle->getAccountID(sfAMMAccount))))) + return nullptr; + else + return sle; +} + +bool +requireAuth(ReadView const& view, Issue const& issue, AccountID const& account) +{ + if (isXRP(issue) || issue.account == account) + return false; + + if (auto const issuerAccount = view.read(keylet::account(issue.account)); + issuerAccount && (*issuerAccount)[sfFlags] & lsfRequireAuth) + { + if (auto const trustLine = + view.read(keylet::line(account, issue.account, issue.currency)); + trustLine) + return !( + (*trustLine)[sfFlags] & + ((account > issue.account) ? lsfLowAuth : lsfHighAuth)); + } + + return false; +} + +std::uint16_t +getTradingFee(SLE const& ammSle, AccountID const& account) +{ + if (ammSle.isFieldPresent(sfAuctionSlot)) + { + auto const& auctionSlot = + static_cast(ammSle.peekAtField(sfAuctionSlot)); + if (auctionSlot.isFieldPresent(sfAccount) && + auctionSlot.getAccountID(sfAccount) == account) + return auctionSlot.getFieldU32(sfDiscountedFee); + if (auctionSlot.isFieldPresent(sfAuthAccounts)) + { + for (auto const& acct : auctionSlot.getFieldArray(sfAuthAccounts)) + if (acct.getAccountID(sfAccount) == account) + return auctionSlot.getFieldU32(sfDiscountedFee); + } + } + return ammSle.getFieldU16(sfTradingFee); +} + +std::pair +getTokensIssue(SLE const& ammSle) +{ + auto const ammToken = + static_cast(ammSle.peekAtField(sfAMMToken)); + auto getIssue = [&](SField const& field) { + auto const token = + static_cast(ammToken.peekAtField(field)); + Issue issue; + issue.currency = token.getFieldH160(sfTokenCurrency); + issue.account = token.getFieldH160(sfTokenIssuer); + return issue; + }; + return {getIssue(sfToken1), getIssue(sfToken2)}; +} + +TER +ammSend( + ApplyView& view, + AccountID const& from, + AccountID const& to, + STAmount const& amount, + beast::Journal j) +{ + if (isXRP(amount)) + return accountSend(view, from, to, amount, j); + + auto const issuer = amount.getIssuer(); + + if (from == issuer || to == issuer || issuer == noAccount()) + { + auto const ter = rippleCredit(view, from, to, amount, false, j); + if (view.rules().enabled(featureDeletableAccounts) && ter != tesSUCCESS) + return ter; + return tesSUCCESS; + } + + TER terResult = rippleCredit(view, issuer, to, amount, true, j); + + if (tesSUCCESS == terResult) + terResult = rippleCredit(view, from, issuer, amount, true, j); + + return terResult; +} + +std::uint16_t +timeSlot(NetClock::time_point const& clock, STObject const& auctionSlot) +{ + using namespace std::chrono; + std::uint32_t constexpr totalSlotTimeSecs = 24 * 3600; + std::uint32_t constexpr intervalDuration = totalSlotTimeSecs / 20; + auto const current = + duration_cast(clock.time_since_epoch()).count(); + if (auctionSlot.isFieldPresent(sfTimeStamp)) + { + auto const stamp = auctionSlot.getFieldU32(sfTimeStamp); + auto const diff = current - stamp; + if (diff < totalSlotTimeSecs) + return diff / intervalDuration; + } + return 0; +} + +bool +ammRequiredAmendments(Rules const& rules) +{ + return rules.enabled(featureAMM) && rules.enabled(fixUniversalNumber) && + rules.enabled(featureFlowCross); +} + +} // namespace ripple diff --git a/src/ripple/app/misc/impl/AMM_formulae.cpp b/src/ripple/app/misc/impl/AMM_formulae.cpp new file mode 100644 index 00000000000..f10f336b386 --- /dev/null +++ b/src/ripple/app/misc/impl/AMM_formulae.cpp @@ -0,0 +1,137 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 + +#include + +namespace ripple { + +STAmount +calcAMMLPT( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue) +{ + auto const tokens = root(asset1 * asset2, 2); + return toSTAmount(lptIssue, tokens); +} + +STAmount +calcLPTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lpTokensBalance, + std::uint16_t tfee) +{ + return toSTAmount( + lpTokensBalance.issue(), + lpTokensBalance * + (root(1 + (asset1Deposit * feeMultHalf(tfee)) / asset1Balance, 2) - + 1)); +} + +STAmount +calcAssetIn( + STAmount const& asset1Balance, + STAmount const& lpTokensBalance, + STAmount const& lptAMMBalance, + std::uint16_t tfee) +{ + return toSTAmount( + asset1Balance.issue(), + ((power(lpTokensBalance / lptAMMBalance + 1, 2) - 1) / + feeMultHalf(tfee)) * + asset1Balance); +} + +STAmount +calcLPTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lpTokensBalance, + std::uint16_t tfee) +{ + if (auto const a = + Number(1) - asset1Withdraw / (asset1Balance * feeMultHalf(tfee)); + a <= 0 || a >= 1) + return STAmount{}; + else + return toSTAmount( + lpTokensBalance.issue(), lpTokensBalance * (1 - root(a, 2))); +} + +STAmount +calcSpotPrice( + STAmount const& asset1Balance, + STAmount const& asset2Balance, + std::uint16_t tfee) +{ + return toSTAmount( + noIssue(), Number{asset2Balance} / (asset1Balance * feeMult(tfee))); +} + +std::optional +changeSpotPrice( + STAmount const& assetInBalance, + STAmount const& assetOutBalance, + STAmount const& newSP, + std::uint16_t tfee) +{ + auto const sp = calcSpotPrice(assetInBalance, assetOutBalance, tfee); + // can't change to a better or same SP + if (Number(newSP) <= sp) + return std::nullopt; + auto const res = assetInBalance * (root(newSP / sp, 2) - 1); + if (res > 0) + return toSTAmount(assetInBalance.issue(), res); + return std::nullopt; +} + +STAmount +calcWithdrawalByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint32_t tfee) +{ + return toSTAmount( + assetBalance.issue(), + assetBalance * (1 - power(1 - lpTokens / lptAMMBalance, 2)) * + feeMultHalf(tfee)); +} + +std::optional +changeSpotPriceQuality( + Amounts const& pool, + Quality const& quality, + std::uint32_t tfee) +{ + auto const curQuality = Quality(pool); + auto const takerPays = + pool.in * (root(quality.rate() / curQuality.rate(), 2) - 1); + if (takerPays > 0) + { + auto const saTakerPays = toSTAmount(pool.in.issue(), takerPays); + return Amounts{saTakerPays, swapAssetIn(pool, saTakerPays, tfee)}; + } + return std::nullopt; +} + +} // namespace ripple diff --git a/src/ripple/app/paths/AMMLiquidity.h b/src/ripple/app/paths/AMMLiquidity.h new file mode 100644 index 00000000000..871da9cadea --- /dev/null +++ b/src/ripple/app/paths/AMMLiquidity.h @@ -0,0 +1,340 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_TX_AMMOFFERMAKER_H_INCLUDED +#define RIPPLE_APP_TX_AMMOFFERMAKER_H_INCLUDED + +#include "ripple/app/misc/AMM.h" +#include "ripple/app/misc/AMM_formulae.h" +#include "ripple/app/paths/AMMOfferCounter.h" +#include "ripple/basics/Log.h" +#include "ripple/ledger/ReadView.h" +#include "ripple/ledger/View.h" +#include "ripple/protocol/Quality.h" +#include "ripple/protocol/STLedgerEntry.h" + +namespace ripple { + +namespace detail { + +/** Generate AMM offers with the offer size based on Fibonacci sequence. + * The sequence corresponds to the payment engine iterations with AMM + * liquidity. Iterations that don't consume AMM offers don't count. + * We max out at four iterations with AMM offers. + */ +class FibSeqHelper +{ +private: + // Current sequence amounts. + mutable Amounts curSeq_{}; + // Latest sequence number. + mutable std::uint16_t lastNSeq_{0}; + mutable Number x_{0}; + mutable Number y_{0}; + +public: + FibSeqHelper() = default; + ~FibSeqHelper() = default; + FibSeqHelper(FibSeqHelper const&) = delete; + FibSeqHelper& + operator=(FibSeqHelper const&) = delete; + /** Generate first sequence. + * @param balances current AMM pool balances. + * @param tfee trading fee in basis points. + * @return + */ + Amounts const& + firstSeq(Amounts const& balances, std::uint16_t tfee) const + { + curSeq_.in = toSTAmount( + balances.in.issue(), + (Number(5) / 10000) * balances.in / 2, + Number::rounding_mode::upward); + curSeq_.out = swapAssetIn(balances, curSeq_.in, tfee); + y_ = curSeq_.out; + return curSeq_; + } + /** Generate next sequence. + * @param n sequence to generate + * @param balances current AMM pool balances. + * @param tfee trading fee in basis points. + * @return + */ + Amounts const& + nextNthSeq(std::uint16_t n, Amounts const& balances, std::uint16_t tfee) + const + { + // We are at the same payment engine iteration when executing + // a limiting step. Have to generate the same sequence. + if (n == lastNSeq_) + return curSeq_; + auto const total = [&]() { + if (n < lastNSeq_) + Throw( + std::string("nextNthSeq: invalid sequence ") + + std::to_string(n) + " " + std::to_string(lastNSeq_)); + Number total{}; + do + { + total = x_ + y_; + x_ = y_; + y_ = total; + } while (++lastNSeq_ < n); + return total; + }(); + curSeq_.out = toSTAmount( + balances.out.issue(), total, Number::rounding_mode::downward); + curSeq_.in = swapAssetOut(balances, curSeq_.out, tfee); + return curSeq_; + } +}; + +} // namespace detail + +/** AMMLiquidity class provides AMM offers to BookStep class. + * The offers are generated in two ways. If there are multiple + * paths specified to the payment transaction then the offers + * are generated based on the Fibonacci sequence with + * at most four payment engine iterations consuming AMM offers. + * These offers behave the same way as CLOB offers in that if + * there is a limiting step, then the offers are adjusted + * based on their quality. + * If there is only one path specified in the payment transaction + * then the offers are generated based on the current step + * remainingIn/remainingOut amounts and/or competing CLOB offer. + * In the latter case, the offer's size is set in such a way + * that the new AMM's pool spot price quality is equal to the CLOB's + * offer quality. + */ +class AMMLiquidity +{ +private: + AMMOfferCounter const& offerCounter_; + AccountID ammAccountID_; + std::uint32_t tradingFee_; + // Cached AMM pool balances as of last getOffers() + mutable Amounts balances_; + // Is seated in case of multi-path. Generates Fibonacci + // sequence offer. + mutable std::optional fibSeqHelper_; + // Indicates that the balances may have changed + // since the last fetchBalances() + mutable bool dirty_; + beast::Journal const j_; + +public: + AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMOfferCounter const& offerCounter, + beast::Journal j); + ~AMMLiquidity() = default; + AMMLiquidity(AMMLiquidity const&) = delete; + AMMLiquidity& + operator=(AMMLiquidity const&) = delete; + + /** Generate AMM offer. Returns nullopt if clobQuality is provided + * and it is better than AMM offer quality. Otherwise returns AMM offer. + * If clobQuality is provided then AMM offer size is set based on the + * quality. If either remainingIn/remainingOut/cache is provided + * then the offer size is adjusted based on those amounts. + */ + template + std::optional + getOffer( + ReadView const& view, + std::optional const& clobQuality = std::nullopt, + std::optional const& remainingIn = std::nullopt, + std::optional const& remainingOut = std::nullopt, + std::optional> const& cache = std::nullopt) const; + + /** Called when AMM offer is consumed. Sets dirty flag + * to indicate that the balances may have changed and + * increments offer counter to indicate that AMM offer + * is used in the strand. + */ + void + consumed() + { + dirty_ = true; + offerCounter_.incrementCounter(); + } + + AccountID + ammAccount() const + { + return ammAccountID_; + } + +private: + /** Fetches AMM balances if dirty flag is set. + */ + Amounts + fetchBalances(ReadView const& view) const; + + /** Returns total amount held by AMM for the given token. + */ + STAmount + ammAccountHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue) const; + + /** Generate offer based on Fibonacci sequence. + * @param balances current AMM balances + */ + Amounts + generateFibSeqOffer(Amounts const& balances) const; +}; + +template +std::optional +AMMLiquidity::getOffer( + ReadView const& view, + std::optional const& clobQuality, + std::optional const& remainingIn, + std::optional const& remainingOut, + std::optional> const& cache) const +{ + // Can't generate more offers. Only applies if generating + // based on Fibonacci sequence. + if (offerCounter_.maxItersReached()) + return std::nullopt; + + auto const balances = fetchBalances(view); + + JLOG(j_.debug()) << "AMMLiquidity::getOffer balances " << balances_.in + << " " << balances_.out << " new balances " << balances.in + << " " << balances.out; + + // Can't generate AMM offer with a better quality than CLOB's offer quality + // if AMM's Spot Price quality is less than CLOB offer quality. + if (clobQuality && Quality{balances} < *clobQuality) + { + JLOG(j_.debug()) << "AMMLiquidity::getOffer, higher clob quality"; + return std::nullopt; + } + + std::optional const saRemIn = remainingIn + ? std::optional(toSTAmount(*remainingIn, balances.in.issue())) + : std::nullopt; + std::optional const saRemOut = remainingOut + ? std::optional( + toSTAmount(*remainingOut, balances.out.issue())) + : std::nullopt; + std::optional const saCacheIn = cache + ? std::optional(toSTAmount(cache->in, balances.in.issue())) + : std::nullopt; + std::optional const saCacheOut = cache + ? std::optional(toSTAmount(cache->out, balances.out.issue())) + : std::nullopt; + + auto const offer = [&]() -> std::optional { + if (offerCounter_.multiPath()) + { + auto const offer = generateFibSeqOffer(balances); + auto const quality = Quality{offer}; + if (clobQuality && quality < clobQuality.value()) + return std::nullopt; + // Change offer size proportionally to the quality + // to retain the strands order by quality. + if (saRemOut && offer.out > *saRemOut) + return quality.ceil_out(offer, *saRemOut); + if (saRemIn && offer.in > *saRemIn) + { + auto amounts = quality.ceil_in(offer, *saRemIn); + // The step produced more output in the forward pass than the + // reverse pass while consuming the same input (or less). + if (saCacheOut && amounts.out > *saCacheOut && + amounts.in <= saCacheIn) + { + amounts = quality.ceil_out(offer, *saCacheOut); + if (amounts.in != *saCacheIn) + return std::nullopt; + } + return amounts; + } + return offer; + } + else if ( + auto const offer = clobQuality + ? changeSpotPriceQuality(balances, *clobQuality, tradingFee_) + : balances) + { + // Change offer size based on swap in/out formulas. The stand's + // quality changes in this case for the better but since + // there is only one strand it doesn't impact the strands order. + if (saRemOut && offer->out > *saRemOut) + return Amounts{ + swapAssetOut(balances, *remainingOut, tradingFee_), + *saRemOut}; + if (saRemIn && offer->in > *saRemIn) + { + auto in = *saRemIn; + auto out = swapAssetIn(balances, *remainingIn, tradingFee_); + // The step produced more output in the forward pass than the + // reverse pass while consuming the same input (or less). + if (saCacheOut && out > *saCacheOut && in <= *saCacheIn) + { + out = *saCacheOut; + in = swapAssetOut(balances, out, tradingFee_); + if (in != *saCacheIn) + return std::nullopt; + } + return Amounts{in, out}; + } + return offer; + } + else + return std::nullopt; + }(); + + balances_ = balances; + + if (offer && offer->in > beast::zero && offer->out > beast::zero) + { + JLOG(j_.debug()) << "AMMLiquidity::getOffer, created " << offer->in + << " " << offer->out; + // The new pool product must be greater or equal to the original pool + // product. Swap in/out formulas are used in case of one-path, which by + // design maintain the product invariant. The FibSeq is also generated + // with the swap in/out formulas except when the offer has to + // be reduced, in which case it is changed proportionally to + // the original offer quality. It can be shown that in this case + // the new pool product is greater than the original pool product. + // Since the result for XRP is fractional, round downward + // out amount and round upward in amount to maintain the invariant. + // This is done in Number/STAmount conversion. + return offer; + } + else + { + JLOG(j_.debug()) << "AMMLiquidity::getOffer, failed " + << offer.has_value(); + } + + return std::nullopt; +} + +} // namespace ripple + +#endif // RIPPLE_APP_TX_AMMOFFERMAKER_H_INCLUDED diff --git a/src/ripple/app/paths/AMMOfferCounter.h b/src/ripple/app/paths/AMMOfferCounter.h new file mode 100644 index 00000000000..3c5b5af061c --- /dev/null +++ b/src/ripple/app/paths/AMMOfferCounter.h @@ -0,0 +1,95 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_PATHS_IMPL_AMMOFFERCOUNTER_H_INCLUDED +#define RIPPLE_APP_PATHS_IMPL_AMMOFFERCOUNTER_H_INCLUDED + +#include + +namespace ripple { + +/** Maintains multiPath_ flag for the payment engine for one-path optimization. + * Maintains counters of amm offers executed at a payment engine iteration + * and the number of iterations that include AMM offers. + * Only one instance of this class is created in Flow.cpp::flow(). + * The reference is percolated through calls to AMMLiquidity class, + * which handles AMM offer generation. + */ +class AMMOfferCounter +{ +private: + // true if payment has multiple paths + bool multiPath_{false}; + // Counter of consumed AMM at payment engine iteration + mutable std::uint16_t ammCounter_{0}; + // Counter of payment engine iterations with consumed AMM + std::uint16_t ammIters_{0}; + +public: + AMMOfferCounter(bool fibSeq) : multiPath_(fibSeq) + { + } + ~AMMOfferCounter() = default; + AMMOfferCounter(AMMOfferCounter const&) = delete; + AMMOfferCounter& + operator=(AMMOfferCounter const&) = delete; + + bool + multiPath() const + { + return multiPath_; + } + + void + setMultiPath(bool fs) + { + multiPath_ = fs; + } + + void + incrementCounter() const + { + if (multiPath_) + ++ammCounter_; + } + + void + updateIters() + { + if (ammCounter_ > 0) + ++ammIters_; + ammCounter_ = 0; + } + + bool + maxItersReached() const + { + return ammIters_ >= 4; + } + + std::uint16_t + curIters() const + { + return ammIters_; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_PATHS_IMPL_AMMOFFERCOUNTER_H_INCLUDED diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index f177cfc1116..05288df6248 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -84,6 +85,9 @@ flow( if (sendMax) sendMaxIssue = sendMax->issue(); + AMMOfferCounter ammOfferCounter( + (defaultPaths && paths.size() == 1) || paths.size() > 1); + // convert the paths to a collection of strands. Each strand is the // collection of account->account steps and book steps that may be used in // this payment. @@ -98,6 +102,7 @@ flow( defaultPaths, ownerPaysTransferFee, offerCrossing, + ammOfferCounter, j); if (toStrandsTer != tesSUCCESS) @@ -145,6 +150,7 @@ flow( limitQuality, sendMax, j, + ammOfferCounter, flowDebugInfo)); } @@ -163,6 +169,7 @@ flow( limitQuality, sendMax, j, + ammOfferCounter, flowDebugInfo)); } @@ -181,6 +188,7 @@ flow( limitQuality, sendMax, j, + ammOfferCounter, flowDebugInfo)); } @@ -198,6 +206,7 @@ flow( limitQuality, sendMax, j, + ammOfferCounter, flowDebugInfo)); } diff --git a/src/ripple/app/paths/impl/AMMLiquidity.cpp b/src/ripple/app/paths/impl/AMMLiquidity.cpp new file mode 100644 index 00000000000..5cfb420fdf5 --- /dev/null +++ b/src/ripple/app/paths/impl/AMMLiquidity.cpp @@ -0,0 +1,103 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 + +namespace ripple { + +AMMLiquidity::AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMOfferCounter const& offerCounter, + beast::Journal j) + : offerCounter_(offerCounter) + , ammAccountID_(ammAccountID) + , tradingFee_(tradingFee) + , balances_{STAmount{in}, STAmount{out}} + , fibSeqHelper_{std::nullopt} + , dirty_(true) + , j_(j) +{ + balances_ = fetchBalances(view); +} + +STAmount +AMMLiquidity::ammAccountHolds( + const ReadView& view, + AccountID const& ammAccountID, + const Issue& issue) const +{ + if (isXRP(issue)) + { + if (auto const sle = view.read(keylet::account(ammAccountID))) + return sle->getFieldAmount(sfBalance); + } + else if (auto const sle = view.read( + keylet::line(ammAccountID, issue.account, issue.currency)); + !isFrozen(view, ammAccountID, issue.currency, issue.account)) + { + auto amount = sle->getFieldAmount(sfBalance); + if (amount.negative()) + amount.negate(); + amount.setIssuer(issue.account); + return amount; + } + + return STAmount{issue}; +} + +Amounts +AMMLiquidity::fetchBalances(const ReadView& view) const +{ + if (dirty_) + { + auto const assetIn = + ammAccountHolds(view, ammAccountID_, balances_.in.issue()); + auto const assetOut = + ammAccountHolds(view, ammAccountID_, balances_.out.issue()); + // This should not happen since AMMLiquidity is created only + // if AMM exists for the given token pair. + if (assetIn <= beast::zero || assetOut <= beast::zero) + Throw("AMMLiquidity: unexpected 0 balances"); + + dirty_ = false; + + return Amounts(assetIn, assetOut); + } + + return balances_; +} + +Amounts +AMMLiquidity::generateFibSeqOffer(const Amounts& balances) const +{ + // first sequence + if (!fibSeqHelper_.has_value()) + { + fibSeqHelper_.emplace(); + return fibSeqHelper_->firstSeq(balances, tradingFee_); + } + // advance to next sequence + return fibSeqHelper_->nextNthSeq( + offerCounter_.curIters(), balances, tradingFee_); +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index a6b2c59611e..fa7d957fbc0 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -17,6 +17,8 @@ */ //============================================================================== +#include +#include #include #include #include @@ -59,6 +61,10 @@ class BookStep : public StepImp> be partially consumed multiple times during a payment. */ std::uint32_t offersUsed_ = 0; + // If set, AMM liquidity might be available + // if AMM offer quality is better than CLOB offer + // quality or there is no CLOB offer. + std::optional ammLiquidity_; beast::Journal const j_; struct Cache @@ -89,8 +95,18 @@ class BookStep : public StepImp> , strandDst_(ctx.strandDst) , prevStep_(ctx.prevStep) , ownerPaysTransferFee_(ctx.ownerPaysTransferFee) + , ammLiquidity_{std::nullopt} , j_(ctx.j) { + if (auto const ammSle = getAMMSle(ctx.view, calcAMMGroupHash(in, out))) + ammLiquidity_.emplace( + ctx.view, + ammSle->getAccountID(sfAMMAccount), + ammSle->getFieldU16(sfTradingFee), + in, + out, + ctx.ammOfferCounter, + ctx.j); } Book const& @@ -132,6 +148,9 @@ class BookStep : public StepImp> qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override; + std::pair, DebtDirection> + getQF(ReadView const& v, DebtDirection prevStepDir) const override; + std::uint32_t offersUsed() const override; @@ -212,6 +231,25 @@ class BookStep : public StepImp> TAmounts const& ofrAmt, TAmounts const& stepAmt, TOut const& ownerGives) const; + + void + consumeAMMOffer(PaymentSandbox& sb, Amounts const& offer); + + // If clobQuality is available and has a better quality then return nullopt, + // otherwise if amm liquidity is available return AMM offer adjusted based + // on remainingIn/Out. + std::optional + getAMMOffer( + ReadView const& view, + std::optional const& clobQuality, + std::optional const& remainingIn = std::nullopt, + std::optional const& remainingOut = std::nullopt) const; + + // If seated then it is either AMM or CLOB quality (whichever is best), + // QualityFunction of the step, and the flag, which is set to true + // if AMM quality is best. + std::optional> + getAMMOrCLOBQuality(ReadView const& view) const; }; //------------------------------------------------------------------------------ @@ -247,7 +285,7 @@ class BookPaymentStep : public BookStep> // A payment can look at offers of any quality bool - checkQualityThreshold(TOffer const& offer) const + checkQualityThreshold(Quality const& quality) const { return true; } @@ -396,9 +434,9 @@ class BookOfferCrossingStep // Offer crossing can prune the offers it needs to look at with a // quality threshold. bool - checkQualityThreshold(TOffer const& offer) const + checkQualityThreshold(Quality const& quality) const { - return !defaultPath_ || offer.quality() >= qualityThreshold_; + return !defaultPath_ || quality >= qualityThreshold_; } // For offer crossing don't pay the transfer fee if alice is paying alice. @@ -479,14 +517,36 @@ BookStep::qualityUpperBound( { auto const dir = this->debtDirection(v, StrandDirection::forward); - // This can be simplified (and sped up) if directories are never empty. - Sandbox sb(&v, tapNONE); - BookTip bt(sb, book_); - if (!bt.step(j_)) + auto const res = getAMMOrCLOBQuality(v); + if (!res) + return {std::nullopt, dir}; + + // Don't adjust if AMM + Quality const q = std::get(*res) + ? std::get(*res) + : static_cast(this)->adjustQualityWithFees( + v, std::get(*res), prevStepDir); + return {q, dir}; +} + +template +std::pair, DebtDirection> +BookStep::getQF( + ReadView const& v, + DebtDirection prevStepDir) const +{ + auto const dir = this->debtDirection(v, StrandDirection::forward); + + auto const res = getAMMOrCLOBQuality(v); + if (!res) return {std::nullopt, dir}; + // Don't adjust if AMM + if (std::get(*res)) + return {std::get(*res), dir}; + Quality const q = static_cast(this)->adjustQualityWithFees( - v, bt.quality(), prevStepDir); + v, std::get(*res), prevStepDir); return {q, dir}; } @@ -625,7 +685,8 @@ BookStep::forEachOffer( } } - if (!static_cast(this)->checkQualityThreshold(offer)) + if (!static_cast(this)->checkQualityThreshold( + offer.quality())) break; auto const ofrInRate = static_cast(this)->getOfrInRate( @@ -705,6 +766,84 @@ BookStep::consumeOffer( offer.consume(sb, ofrAmt); } +template +std::optional +BookStep::getAMMOffer( + ReadView const& view, + std::optional const& clobQuality, + std::optional const& remainingIn, + std::optional const& remainingOut) const +{ + if (ammLiquidity_) + { + auto const cache = cache_ ? std::optional>( + TAmounts{cache_->in, cache_->out}) + : std::nullopt; + return ammLiquidity_->getOffer( + view, clobQuality, remainingIn, remainingOut, cache); + } + return std::nullopt; +} + +template +std::optional> +BookStep::getAMMOrCLOBQuality(ReadView const& view) const +{ + // This can be simplified (and sped up) if directories are never empty. + Sandbox sb(&view, tapNONE); + BookTip bt(sb, book_); + auto const clobQuality = + bt.step(j_) ? std::optional(bt.quality()) : std::nullopt; + // Don't pass in clobQuality. For one-path it returns the offer as + // the pool balances and the resulting quality is Spot Price Quality. + // For multi-path it returns the actual offer. + if (auto const ammOffer = getAMMOffer(view, std::nullopt)) + { + auto const ammQ{Quality{*ammOffer}}; + // AMM quality is better or no CLOB offer + if ((clobQuality && ammQ > *clobQuality) || !clobQuality) + return std::make_tuple(ammQ, QualityFunction{*ammOffer}, true); + } + // CLOB quality is better or no AMM offer + if (clobQuality) + return std::make_tuple( + *clobQuality, QualityFunction{*clobQuality}, false); + // Neither CLOB nor AMM offer is available + return std::nullopt; +} + +template +void +BookStep::consumeAMMOffer( + PaymentSandbox& sb, + Amounts const& offer) +{ + assert(ammLiquidity_); + + if (auto const res = ammSend( + sb, + offer.in.issue().account, + ammLiquidity_->ammAccount(), + offer.in, + j_); + res != tesSUCCESS) + Throw(res); + + if (auto const res = ammSend( + sb, + ammLiquidity_->ammAccount(), + offer.out.issue().account, + offer.out, + j_); + res != tesSUCCESS) + Throw(res); + + JLOG(j_.trace()) << "AMMLiquidity::consume " << offer.in << " " + << offer.out; + + ammLiquidity_->consumed(); +} + template static auto sum(TCollection const& col) @@ -734,6 +873,45 @@ BookStep::revImp( boost::container::flat_multiset savedOuts; savedOuts.reserve(64); + bool triedAMM = false; + + // Consume AMM offer if it is available and has a better + // quality than CLOB offer quality. AMM offer can only + // be consumed once at a given CLOB quality. Return true + // if AMM offer is consumed and has a better quality than + // CLOB offer quality. Return false if AMM offer is not + // available, or if it has the same quality as CLOB offer. + auto tryAMM = [&](std::optional const& clobQuality) { + triedAMM = true; + if (auto const offer = + getAMMOffer(sb, clobQuality, std::nullopt, remainingOut)) + { + auto const quality = Quality{*offer}; + if (!static_cast(this)->checkQualityThreshold( + quality)) + return false; + + consumeAMMOffer(sb, *offer); + + remainingOut -= get(offer->out); + // AMM and offer have the same quality. + // The offer stream can continue consuming + // CLOB offers at the same quality. + if (clobQuality && quality == *clobQuality) + { + savedIns.insert(get(offer->in)); + savedOuts.insert(get(offer->out)); + } + else + { + result.in = get(offer->in); + result.out = get(offer->out); + return true; + } + } + return false; + }; + /* amt fed will be adjusted by owner funds (and may differ from the offer's amounts - tho always <=) Return true to continue to receive offers, false to stop receiving offers. @@ -747,6 +925,10 @@ BookStep::revImp( if (remainingOut <= beast::zero) return false; + // AMM offer is available and has a better quality + if (!triedAMM && tryAMM(offer.quality())) + return false; + if (stpAmt.out <= remainingOut) { savedIns.insert(stpAmt.in); @@ -798,6 +980,9 @@ BookStep::revImp( std::uint32_t const offersConsumed = std::get<1>(r); offersUsed_ = offersConsumed; SetUnion(ofrsToRm, toRm); + // CLOB offer is not available, try AMM + if (!triedAMM) + tryAMM(std::nullopt); if (offersConsumed >= maxOffersToConsume_) { @@ -855,6 +1040,43 @@ BookStep::fwdImp( boost::container::flat_multiset savedOuts; savedOuts.reserve(64); + bool triedAMM = false; + + // Consume AMM offer if it is available and has a better + // quality than CLOB offer quality. AMM offer can only + // be consumed once at a given CLOB quality. Return true + // if AMM offer is consumed and has a better quality than + // CLOB offer quality. Return false if AMM offer is not + // available, or it has the same quality as CLOB offer. + auto tryAMM = [&](std::optional const& clobQuality) { + triedAMM = true; + if (auto const offer = + getAMMOffer(sb, clobQuality, remainingIn, std::nullopt)) + { + auto const quality = Quality{*offer}; + if (!static_cast(this)->checkQualityThreshold( + quality)) + return false; + + consumeAMMOffer(sb, *offer); + + remainingIn -= get(offer->in); + // AMM and CLOB offer have the same quality + if (quality == clobQuality) + { + savedIns.insert(get(offer->in)); + savedOuts.insert(get(offer->out)); + } + else + { + result.in = get(offer->in); + result.out = get(offer->out); + return true; + } + } + return false; + }; + // amt fed will be adjusted by owner funds (and may differ from the offer's // amounts - tho always <=) auto eachOffer = [&](TOffer& offer, @@ -868,6 +1090,10 @@ BookStep::fwdImp( if (remainingIn <= beast::zero) return false; + // AMM offer is available and has a better quality + if (!triedAMM && tryAMM(offer.quality())) + return false; + bool processMore = true; auto ofrAdjAmt = ofrAmt; auto stpAdjAmt = stpAmt; @@ -968,6 +1194,9 @@ BookStep::fwdImp( std::uint32_t const offersConsumed = std::get<1>(r); offersUsed_ = offersConsumed; SetUnion(ofrsToRm, toRm); + // CLOB offer is not available, try AMM + if (!triedAMM) + tryAMM(std::nullopt); if (offersConsumed >= maxOffersToConsume_) { diff --git a/src/ripple/app/paths/impl/PaySteps.cpp b/src/ripple/app/paths/impl/PaySteps.cpp index 578d73fb76b..a25b3028a1e 100644 --- a/src/ripple/app/paths/impl/PaySteps.cpp +++ b/src/ripple/app/paths/impl/PaySteps.cpp @@ -142,6 +142,7 @@ toStrand( STPath const& path, bool ownerPaysTransferFee, bool offerCrossing, + AMMOfferCounter& ammOfferCounter, beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || @@ -278,6 +279,7 @@ toStrand( isDefaultPath, seenDirectIssues, seenBookOuts, + ammOfferCounter, j}; }; @@ -474,6 +476,7 @@ toStrands( bool addDefaultPath, bool ownerPaysTransferFee, bool offerCrossing, + AMMOfferCounter& ammOfferCounter, beast::Journal j) { std::vector result; @@ -499,6 +502,7 @@ toStrands( STPath(), ownerPaysTransferFee, offerCrossing, + ammOfferCounter, j); auto const ter = sp.first; auto& strand = sp.second; @@ -542,6 +546,7 @@ toStrands( p, ownerPaysTransferFee, offerCrossing, + ammOfferCounter, j); auto ter = sp.first; auto& strand = sp.second; @@ -587,6 +592,7 @@ StrandContext::StrandContext( bool isDefaultPath_, std::array, 2>& seenDirectIssues_, boost::container::flat_set& seenBookOuts_, + AMMOfferCounter& ammOfferCounter_, beast::Journal j_) : view(view_) , strandSrc(strandSrc_) @@ -602,6 +608,7 @@ StrandContext::StrandContext( , prevStep(!strand_.empty() ? strand_.back().get() : nullptr) , seenDirectIssues(seenDirectIssues_) , seenBookOuts(seenBookOuts_) + , ammOfferCounter(ammOfferCounter_) , j(j_) { } diff --git a/src/ripple/app/paths/impl/Steps.h b/src/ripple/app/paths/impl/Steps.h index 7bebd18999f..c5092f2f2b5 100644 --- a/src/ripple/app/paths/impl/Steps.h +++ b/src/ripple/app/paths/impl/Steps.h @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ namespace ripple { class PaymentSandbox; class ReadView; class ApplyView; +class AMMOfferCounter; enum class DebtDirection { issues, redeems }; enum class QualityDirection { in, out }; @@ -188,6 +190,22 @@ class Step virtual std::pair, DebtDirection> qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const = 0; + /** Get QualityFunction. Used in one path optimization where + * the quality function is non-constant (has AMM) and there is + * limitQuality. QualityFunction allows calculation of + * required path output given requested limitQuality. + * All steps, except for BookStep have the default + * implementation. + */ + virtual std::pair, DebtDirection> + getQF(ReadView const& v, DebtDirection prevStepDir) const + { + if (auto const res = qualityUpperBound(v, prevStepDir); res.first) + return {QualityFunction{*res.first}, res.second}; + else + return {std::nullopt, res.second}; + } + /** Return the number of offers consumed or partially consumed the last time the step ran, including expired and unfunded offers. @@ -361,6 +379,7 @@ normalizePath( @param ownerPaysTransferFee false -> charge sender; true -> charge offer owner @param offerCrossing false -> payment; true -> offer crossing + @param ammOfferCounter counts iterations with AMM offers @param j Journal for logging messages @return Error code and constructed Strand */ @@ -375,6 +394,7 @@ toStrand( STPath const& path, bool ownerPaysTransferFee, bool offerCrossing, + AMMOfferCounter& ammOfferCounter, beast::Journal j); /** @@ -398,6 +418,7 @@ toStrand( @param ownerPaysTransferFee false -> charge sender; true -> charge offer owner @param offerCrossing false -> payment; true -> offer crossing + @param ammOfferCounter counts iterations with AMM offers @param j Journal for logging messages @return error code and collection of strands */ @@ -413,6 +434,7 @@ toStrands( bool addDefaultPath, bool ownerPaysTransferFee, bool offerCrossing, + AMMOfferCounter& ammOfferCounter, beast::Journal j); /// @cond INTERNAL @@ -521,6 +543,7 @@ struct StrandContext than once */ boost::container::flat_set& seenBookOuts; + AMMOfferCounter& ammOfferCounter; beast::Journal const j; /** StrandContext constructor. */ @@ -540,7 +563,8 @@ struct StrandContext std::array, 2>& seenDirectIssues_, ///< For detecting currency loops boost::container::flat_set& - seenBookOuts_, ///< For detecting book loops + seenBookOuts_, ///< For detecting book loops + AMMOfferCounter& ammOfferCounter_, beast::Journal j_); ///< Journal for logging }; diff --git a/src/ripple/app/paths/impl/StrandFlow.h b/src/ripple/app/paths/impl/StrandFlow.h index 487455f690f..bbec96460f3 100644 --- a/src/ripple/app/paths/impl/StrandFlow.h +++ b/src/ripple/app/paths/impl/StrandFlow.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_APP_PATHS_IMPL_STRANDFLOW_H_INCLUDED #define RIPPLE_APP_PATHS_IMPL_STRANDFLOW_H_INCLUDED +#include #include #include #include @@ -349,6 +350,53 @@ qualityUpperBound(ReadView const& v, Strand const& strand) }; /// @endcond +/// @cond INTERNAL +/** Limit remaining out only if one strand and limitQuality is included. + * Targets one path payment with AMM where the average quality is linear + * and instant quality is quadratic function of output. Calculating quality + * function for the whole strand enables figuring out required output + * to produce requested strand's limitQuality. Reducing the output, + * increases quality of AMM steps, increasing the strand's composite + * quality as the result. + */ +template +inline TOutAmt +limitOut( + ReadView const& v, + Strand const& strand, + TOutAmt const& remainingOut, + Quality const& limitQuality) +{ + std::optional stepQF; + QualityFunction qf; + DebtDirection dir = DebtDirection::issues; + for (auto const& step : strand) + { + if (std::tie(stepQF, dir) = step->getQF(v, dir); stepQF) + qf.combineWithNext(*stepQF); + else + return remainingOut; + } + + // QualityFunction is constant + if (qf.isConst()) + return remainingOut; + + auto const out = [&]() { + if (auto const out = qf.outFromInstQ(limitQuality); !out) + return remainingOut; + else if constexpr (std::is_same_v) + return (XRPAmount)*out; + else if constexpr (std::is_same_v) + return IOUAmount{*out}; + else + return STAmount{ + remainingOut.issue(), out->mantissa(), out->exponent()}; + }(); + return out < remainingOut ? out : remainingOut; +}; +/// @endcond + /// @cond INTERNAL /* Track the non-dry strands @@ -487,6 +535,7 @@ class ActiveStrands @param limitQuality If present, the minimum quality for any strand taken @param sendMaxST If present, the maximum STAmount to send @param j Journal to write journal messages to + @param ammOfferCounter counts iterations with AMM offers @param flowDebugInfo If pointer is non-null, write flow debug info here @return Actual amount in and out from the strands, errors, and payment sandbox @@ -502,6 +551,7 @@ flow( std::optional const& limitQuality, std::optional const& sendMaxST, beast::Journal j, + AMMOfferCounter& ammOfferCounter, path::detail::FlowDebugInfo* flowDebugInfo = nullptr) { // Used to track the strand that offers the best quality (output/input @@ -585,6 +635,16 @@ flow( activeStrands.activateNext(sb, limitQuality); + ammOfferCounter.setMultiPath(activeStrands.size() > 1); + + // Limit only if one strand and limitQuality + auto const limitRemainingOut = [&]() { + if (activeStrands.size() == 1 && limitQuality) + if (auto const strand = activeStrands.get(0)) + return limitOut(sb, *strand, remainingOut, *limitQuality); + return remainingOut; + }(); + boost::container::flat_set ofrsToRm; std::optional best; if (flowDebugInfo) @@ -611,7 +671,7 @@ flow( continue; } auto f = flow( - sb, *strand, remainingIn, remainingOut, j); + sb, *strand, remainingIn, limitRemainingOut, j); // rm bad offers even if the strand fails SetUnion(ofrsToRm, f.ofrsToRm); @@ -708,6 +768,7 @@ flow( << " remainingOut: " << to_string(remainingOut); best->sb.apply(sb); + ammOfferCounter.updateIters(); } else { diff --git a/src/ripple/app/tx/impl/AMMBid.cpp b/src/ripple/app/tx/impl/AMMBid.cpp new file mode 100644 index 00000000000..a93561313a3 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMBid.cpp @@ -0,0 +1,313 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +AMMBid::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx}; +} + +NotTEC +AMMBid::preflight(PreflightContext const& ctx) +{ + if (!ammRequiredAmendments(ctx.rules)) + return temDISABLED; + + auto const ret = preflight1(ctx); + if (!isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "AMM Bid: invalid flags."; + return temINVALID_FLAG; + } + + if (ctx.tx[~sfMinSlotPrice] && ctx.tx[~sfMaxSlotPrice]) + { + JLOG(ctx.j.debug()) << "AMM Bid: invalid options."; + return temBAD_AMM_OPTIONS; + } + + if (invalidAmount(ctx.tx[~sfMinSlotPrice]) || + invalidAmount(ctx.tx[~sfMaxSlotPrice])) + { + JLOG(ctx.j.debug()) << "AMM Bid: invalid min slot price."; + return temBAD_AMM_TOKENS; + } + + return preflight2(ctx); +} + +TER +AMMBid::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.read(keylet::account(ctx.tx[sfAccount]))) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid account."; + return terNO_ACCOUNT; + } + + auto const ammSle = getAMMSle(ctx.view, ctx.tx[sfAMMID]); + if (!ammSle) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid AMM account."; + return terNO_ACCOUNT; + } + + if (ctx.tx.isFieldPresent(sfAuthAccounts)) + { + auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts); + if (authAccounts.size() > 4) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; + return temBAD_AMM_OPTIONS; + } + + for (auto& account : authAccounts) + { + if (!ctx.view.read( + keylet::account(account.getAccountID(sfAccount)))) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account."; + return terNO_ACCOUNT; + } + } + } + + auto const lpTokens = lpHolds( + ctx.view, ammSle->getAccountID(sfAMMAccount), ctx.tx[sfAccount], ctx.j); + + if (auto const minSlotPrice = ctx.tx[~sfMinSlotPrice]) + { + if (*minSlotPrice > lpTokens) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens."; + return tecAMM_INVALID_TOKENS; + } + if (minSlotPrice->issue() != lpTokens.issue()) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken."; + return temBAD_AMM_TOKENS; + } + } + + if (auto const maxSlotPrice = ctx.tx[~sfMaxSlotPrice]) + { + if (*maxSlotPrice > lpTokens) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens."; + return tecAMM_INVALID_TOKENS; + } + if (maxSlotPrice->issue() != lpTokens.issue()) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken."; + return temBAD_AMM_TOKENS; + } + } + + return tesSUCCESS; +} + +void +AMMBid::preCompute() +{ + return Transactor::preCompute(); +} + +std::pair +AMMBid::applyGuts(Sandbox& sb) +{ + using namespace std::chrono; + auto const amm = getAMMSle(sb, ctx_.tx[sfAMMID]); + assert(amm); + auto const ammAccount = amm->getAccountID(sfAMMAccount); + auto const lptAMMBalance = amm->getFieldAmount(sfLPTokenBalance); + auto const lpTokens = lpHolds(sb, ammAccount, account_, ctx_.journal); + if (!amm->isFieldPresent(sfAuctionSlot)) + amm->makeFieldPresent(sfAuctionSlot); + auto& auctionSlot = amm->peekFieldObject(sfAuctionSlot); + auto const current = + duration_cast( + ctx_.view().info().parentCloseTime.time_since_epoch()) + .count(); + + std::uint32_t constexpr totalSlotTimeSecs = 24 * 3600; + std::uint32_t constexpr nIntervals = 20; + std::uint32_t constexpr intervalDuration = totalSlotTimeSecs / nIntervals; + + // If seated then it is the current slot-holder time slot, otherwise + // the auction slot is not owned. Slot range is in {0-19} + auto const timeSlot = [&]() -> std::optional { + if (auctionSlot.isFieldPresent(sfTimeStamp)) + { + auto const stamp = auctionSlot.getFieldU32(sfTimeStamp); + auto const diff = current - stamp; + if (diff < totalSlotTimeSecs) + return (std::int64_t)(diff / intervalDuration); + } + return std::nullopt; + }(); + + // Account must exist, is LP, and the slot not expired. + auto validOwner = [&](AccountID const& account) { + return sb.read(keylet::account(account)) && + lpHolds(sb, ammAccount, account, ctx_.journal) != beast::zero && + timeSlot && *timeSlot < 19; + }; + + auto updateSlot = [&](std::uint32_t fee, + Number const& minPrice, + Number const& burn) -> TER { + auctionSlot.setAccountID(sfAccount, account_); + auctionSlot.setFieldU32(sfTimeStamp, current); + auctionSlot.setFieldU32(sfDiscountedFee, fee); + auctionSlot.setFieldAmount( + sfPrice, toSTAmount(lpTokens.issue(), minPrice)); + if (ctx_.tx.isFieldPresent(sfAuthAccounts)) + auctionSlot.setFieldArray( + sfAuthAccounts, ctx_.tx.getFieldArray(sfAuthAccounts)); + // Burn the remaining bid amount + auto const saBurn = toSTAmount(lpTokens.issue(), burn); + auto res = + redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem."; + return res; + } + amm->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn); + sb.update(amm); + return tesSUCCESS; + }; + + TER res = tesSUCCESS; + + auto const minSlotPrice = ctx_.tx[~sfMinSlotPrice]; + auto const maxSlotPrice = ctx_.tx[~sfMaxSlotPrice]; + + Number const MinSlotPrice = lptAMMBalance / 100000; // 0.001% TBD + // Arbitrager's bid price + auto const bidPrice = [&]() -> Number { + if (minSlotPrice) + return *minSlotPrice; + else if (maxSlotPrice) + return *maxSlotPrice; + else + return 0; + }(); + + // No one owns the slot or expired slot. + // The bidder pays MinSlotPrice + if (!auctionSlot.isFieldPresent(sfAccount) || + !validOwner(auctionSlot.getAccountID(sfAccount))) + { + res = updateSlot(0, MinSlotPrice, MinSlotPrice); + } + else + { + // Price the slot was purchased at. + Number const pricePurchased = auctionSlot.getFieldAmount(sfPrice); + auto const fractionUsed = (Number(*timeSlot) + 1) / nIntervals; + auto const fractionRemaining = Number(1) - fractionUsed; + auto computedPrice = [&]() -> Number { + Number const p1_05 = Number(105) / 100; + // First interval slot price + if (*timeSlot == 0) + return pricePurchased * p1_05; + // Other intervals slot price + else + return pricePurchased * p1_05 * (1 - power(fractionUsed, 60)) + + MinSlotPrice; + }(); + + // If max pricePurchased then don't pay more than the max + // pricePurchased. + if (maxSlotPrice && computedPrice > *maxSlotPrice) + { + JLOG(ctx_.journal.debug()) << "AMM Bid: computed pricePurchased " + "exceeds max pricePurchased."; + return {tecAMM_FAILED_BID, false}; + } + + auto const payPrice = [&]() -> Number { + // Bidder pays max(bidPrice, computedPrice) + if (minSlotPrice) + return bidPrice > computedPrice ? bidPrice : computedPrice; + else if (maxSlotPrice) + return bidPrice; // max slot price is less than computed price + else + return computedPrice; + }(); + + res = updateSlot(0, payPrice, payPrice * (1 - fractionRemaining)); + if (res != tesSUCCESS) + return {res, false}; + // Refund the previous owner. If the time slot is 0 then + // the owner is refunded full amount. + res = accountSend( + sb, + account_, + auctionSlot.getAccountID(sfAccount), + toSTAmount(lpTokens.issue(), fractionRemaining * payPrice), + ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) << "AMM Bid: failed to refund."; + return {res, false}; + } + } + + return {tesSUCCESS, true}; +} + +TER +AMMBid::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + // This is a ledger with just the fees paid and any unfunded or expired + // offers we encounter removed. It's used when handling Fill-or-Kill offers, + // if the order isn't going to be placed, to avoid wasting the work we did. + Sandbox sbCancel(&ctx_.view()); + + auto const result = applyGuts(sb); + if (result.second) + sb.apply(ctx_.rawView()); + else + sbCancel.apply(ctx_.rawView()); + + return result.first; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/AMMBid.h b/src/ripple/app/tx/impl/AMMBid.h new file mode 100644 index 00000000000..16c319a028c --- /dev/null +++ b/src/ripple/app/tx/impl/AMMBid.h @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMBID_H_INCLUDED +#define RIPPLE_TX_AMMBID_H_INCLUDED + +#include + +namespace ripple { + +class Sandbox; + +/** AMMBid implements AMM bid Transactor. + * This is a novel mechanism for an AMM instance to auction-off + * the trading advantages to users (arbitrageurs) at a discounted + * TradingFee for a 24 hour slot. Any account that owns corresponding + * LPTokens can bid for the auction slot of that AMM instance. + * Part of the proceeds from the auction, i.e. LPTokens are refunded + * to the current slot-holder computed on a pro rata basis. + * Remaining part of the proceeds - in the units of LPTokens- is burnt, + * thus effectively increasing the LPs shares. + * Total slot time of 24 hours is divided into 20 equal intervals. + * The auction slot can be in any of the following states at any time: + * - Empty - no account currently holds the slot. + * - Occupied - an account owns the slot with at least 5% of the remaining + * slot time (in one of 1-19 intervals). + * - Tailing - an account owns the slot with less than 5% of the remaining time. + * The slot-holder owns the slot privileges when in state Occupied or Tailing. + * If x is the fraction of used slot time for the current slot holder + * and X is the price at which the slot can be bought specified in LPTokens + * then: The minimum bid price for the slot in first interval is f(x) = X * 1.05 + * The bid price of slot any time is + * f(x) = X * 1.05 * (1 - x^60) + min_slot_price, where min_slot_price + * is some constant minimum slot price. + * The revenue from a successful bid is split between the current slot-holder + * and the pool. The current slot holder is always refunded the remaining slot + * value f(x) = (1 - x) * X. + * The remaining LPTokens are burnt. + * The auction information is maintained in AuctionSlot of ltAMM object. + * AuctionSlot contains: + * Account - account id, which owns the slot. + * TimeStamp - the time (since ripple epoch) when slot was bought. + * DiscountedFee - trading fee charged to the account, default is 0. + * Price - price paid for the slot in LPTokens. + * AuthAccounts - up to four accounts authorized to trade at + * the discounted fee. + */ +class AMMBid : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit AMMBid(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static NotTEC + preflight(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Gather information beyond what the Transactor base class gathers. */ + void + preCompute() override; + + /** Attempt to create the AMM instance. */ + TER + doApply() override; + +private: + std::pair + applyGuts(Sandbox& view); +}; + +} // namespace ripple + +#endif // RIPPLE_TX_AMMBID_H_INCLUDED diff --git a/src/ripple/app/tx/impl/AMMCreate.cpp b/src/ripple/app/tx/impl/AMMCreate.cpp new file mode 100644 index 00000000000..75a481e1e07 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMCreate.cpp @@ -0,0 +1,254 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +AMMCreate::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx}; +} + +NotTEC +AMMCreate::preflight(PreflightContext const& ctx) +{ + if (!ammRequiredAmendments(ctx.rules)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "AMM Instance: invalid flags."; + return temINVALID_FLAG; + } + + auto const saAsset1 = ctx.tx[sfAsset1]; + auto const saAsset2 = ctx.tx[sfAsset2]; + + if (saAsset1.issue() == saAsset2.issue()) + { + JLOG(ctx.j.debug()) + << "AMM Instance: tokens can not have the same currency/issuer."; + return temBAD_AMM_TOKENS; + } + + if (auto const err = invalidAmount(saAsset1)) + { + JLOG(ctx.j.debug()) << "AMM Instance: invalid asset1 amount."; + return *err; + } + + if (auto const err = invalidAmount(saAsset2)) + { + JLOG(ctx.j.debug()) << "AMM Instance: invalid asset2 amount."; + return *err; + } + + if (ctx.tx[sfTradingFee] > 65000) + { + JLOG(ctx.j.debug()) << "AMM Instance: invalid trading fee."; + return temBAD_FEE; + } + + return preflight2(ctx); +} + +TER +AMMCreate::preclaim(PreclaimContext const& ctx) +{ + auto const accountID = ctx.tx[sfAccount]; + auto const saAsset1 = ctx.tx[sfAsset1]; + auto const saAsset2 = ctx.tx[sfAsset2]; + + if (!ctx.view.read(keylet::account(accountID))) + { + JLOG(ctx.j.debug()) << "AMM Instance: Invalid account."; + return terNO_ACCOUNT; + } + + if (requireAuth(ctx.view, saAsset1.issue(), accountID) || + requireAuth(ctx.view, saAsset2.issue(), accountID)) + { + JLOG(ctx.j.debug()) << "AMM Instance: account is not authorized"; + return tecNO_PERMISSION; + } + + if (isFrozen(ctx.view, saAsset1) || isFrozen(ctx.view, saAsset2)) + { + JLOG(ctx.j.debug()) << "AMM Instance: involves frozen asset."; + return tecFROZEN; + } + + auto insufficientBalance = [&](STAmount const& asset) { + auto const balance = accountHolds( + ctx.view, + accountID, + asset.issue().currency, + asset.issue().account, + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j); + return balance < asset; + }; + + if (insufficientBalance(saAsset1) || insufficientBalance(saAsset2)) + { + JLOG(ctx.j.debug()) << "AMM Instance: has insufficient funds"; + return tecUNFUNDED_PAYMENT; + } + + return tesSUCCESS; +} + +void +AMMCreate::preCompute() +{ + return Transactor::preCompute(); +} + +std::pair +AMMCreate::applyGuts(Sandbox& sb) +{ + auto const saAsset1 = ctx_.tx[sfAsset1]; + auto const saAsset2 = ctx_.tx[sfAsset2]; + + auto const ammID = calcAMMGroupHash(saAsset1.issue(), saAsset2.issue()); + + // Check if AMM already exists for the token pair + if (sb.peek(keylet::amm(ammID))) + { + JLOG(j_.debug()) << "AMM Instance: ltAMM already exists."; + return {tecAMM_EXISTS, false}; + } + + auto const ammAccountID = calcAccountID(sb.info().parentHash, ammID); + + // AMM account already exists (should not happen) + if (sb.peek(keylet::account(ammAccountID))) + { + JLOG(j_.debug()) << "AMM Instance: AMM already exists."; + return {tecAMM_EXISTS, false}; + } + + // LP Token already exists. (should not happen) + auto const lptIssue = calcLPTIssue(ammAccountID); + if (sb.read(keylet::line(ammAccountID, lptIssue))) + { + JLOG(j_.debug()) << "AMM Instance: LP Token already exists."; + return {tecAMM_EXISTS, false}; + } + + // Create AMM Root Account. + auto sleAMMRoot = std::make_shared(keylet::account(ammAccountID)); + sleAMMRoot->setAccountID(sfAccount, ammAccountID); + sleAMMRoot->setFieldAmount(sfBalance, STAmount{}); + std::uint32_t const seqno{ + view().rules().enabled(featureDeletableAccounts) ? view().seq() : 1}; + sleAMMRoot->setFieldU32(sfSequence, seqno); + // Ignore reserves requirement, disable the master key, and allow default + // rippling (AMM LPToken can be used as a token in another AMM, which must + // support payments and offer crossing). + sleAMMRoot->setFieldU32( + sfFlags, lsfAMM | lsfDisableMaster | lsfDefaultRipple); + sb.insert(sleAMMRoot); + + // Calculate initial LPT balance. + auto const lpTokens = calcAMMLPT(saAsset1, saAsset2, lptIssue); + + // Create ltAMM + auto ammSle = std::make_shared(keylet::amm(ammID)); + ammSle->setFieldU16(sfTradingFee, ctx_.tx[sfTradingFee]); + ammSle->setAccountID(sfAMMAccount, ammAccountID); + ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); + auto const& issue1 = saAsset1.issue() < saAsset2.issue() ? saAsset1.issue() + : saAsset2.issue(); + auto const& issue2 = + issue1 == saAsset1.issue() ? saAsset2.issue() : saAsset1.issue(); + ammSle->makeFieldPresent(sfAMMToken); + auto& ammToken = ammSle->peekFieldObject(sfAMMToken); + auto setToken = [&](SField const& field, Issue const& issue) { + ammToken.makeFieldPresent(field); + auto& token = ammToken.peekFieldObject(field); + token.setFieldH160(sfTokenCurrency, issue.currency); + token.setFieldH160(sfTokenIssuer, issue.account); + }; + setToken(sfToken1, issue1); + setToken(sfToken2, issue2); + sb.insert(ammSle); + + // Send LPT to LP. + auto res = accountSend(sb, ammAccountID, account_, lpTokens, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(j_.debug()) << "AMM Instance: failed to send LPT " << lpTokens; + return {res, false}; + } + + // Send asset1. + res = ammSend(sb, account_, ammAccountID, saAsset1, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(j_.debug()) << "AMM Instance: failed to send " << saAsset1; + return {res, false}; + } + + // Send asset2. + res = ammSend(sb, account_, ammAccountID, saAsset2, ctx_.journal); + if (res != tesSUCCESS) + JLOG(j_.debug()) << "AMM Instance: failed to send " << saAsset2; + else + JLOG(j_.debug()) << "AMM Instance: success " << ammAccountID << " " + << ammID << " " << lptIssue << " " << saAsset1 << " " + << saAsset2; + + return {res, res == tesSUCCESS}; +} + +TER +AMMCreate::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + // This is a ledger with just the fees paid and any unfunded or expired + // offers we encounter removed. It's used when handling Fill-or-Kill offers, + // if the order isn't going to be placed, to avoid wasting the work we did. + Sandbox sbCancel(&ctx_.view()); + + auto const result = applyGuts(sb); + if (result.second) + sb.apply(ctx_.rawView()); + else + sbCancel.apply(ctx_.rawView()); + + return result.first; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/tx/impl/AMMCreate.h b/src/ripple/app/tx/impl/AMMCreate.h new file mode 100644 index 00000000000..593c8aec0d6 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMCreate.h @@ -0,0 +1,93 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMCREATE_H_INCLUDED +#define RIPPLE_TX_AMMCREATE_H_INCLUDED + +#include + +namespace ripple { + +class Sandbox; + +/** AMMCreate implements Automatic Market Maker(AMM) creation Transactor. + * [https://github.com/XRPLF/XRPL-Standards/discussions/78] + * It creates a new AMM instance with two tokens. Any trader, or Liquidity + * Provider (LP), can create the AMM instance and receive in return shares + * of the AMM pool in the form on LPTokens. The number of tokens that LP gets + * are determined by LPTokens = sqrt(A * B), where A and B is the current + * composition of the AMM pool. LP can add (AMMDeposit) or withdraw + * (AMMWithdraw) tokens from AMM and + * AMM can be used transparently in the payment or offer crossing transactions. + * Trading fee is charged to the traders for the trades executed against + * AMM instance. The fee is added to the AMM pool and distributed to the LPs + * in proportion to the LPTokens upon liquidity removal. The fee can be voted + * on by LP's (AMMVote). LP's can continuously bid (AMMBid) for the 24 hour + * auction slot, which enables LP's to trade at zero trading fee. + * AMM instance creates AccountRoot object with disabled master key + * for book-keeping of XRP balance if one of the tokens + * is XRP, a trustline for each IOU token, a trustline to keep track + * of LPTokens, and ltAMM ledger object. AccountRoot ID is generated + * internally from the parent's hash. ltAMM's object ID is + * hash{token1.currency, token1.issuer, token2.currency, token2.issuer}, where + * issue1 < issue2. ltAMM object provides mapping from the hash to AccountRoot + * ID and contains: AMMAccount - AMM AccountRoot ID. TradingFee - AMM voted + * TradingFee. VoteSlots - Array of VoteEntry, contains fee vote information. + * AuctionSlot - Auction slot, contains discounted fee bid information. + * LPTokenBalance - LPTokens outstanding balance. + * AMMToken - currency/issuer information for AMM tokens. + * AMMDeposit, AMMWithdraw, AMMVote, and AMMBid transactions use the hash + * to access AMM instance. + */ +class AMMCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit AMMCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static NotTEC + preflight(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Gather information beyond what the Transactor base class gathers. */ + void + preCompute() override; + + /** Attempt to create the AMM instance. */ + TER + doApply() override; + +private: + std::pair + applyGuts(Sandbox& view); +}; + +} // namespace ripple + +#endif // RIPPLE_TX_AMMCREATE_H_INCLUDED diff --git a/src/ripple/app/tx/impl/AMMDeposit.cpp b/src/ripple/app/tx/impl/AMMDeposit.cpp new file mode 100644 index 00000000000..6fd8a9084c2 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMDeposit.cpp @@ -0,0 +1,511 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2022 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +AMMDeposit::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx}; +} + +NotTEC +AMMDeposit::preflight(PreflightContext const& ctx) +{ + if (!ammRequiredAmendments(ctx.rules)) + return temDISABLED; + + auto const ret = preflight1(ctx); + if (!isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid flags."; + return temINVALID_FLAG; + } + + auto const asset1In = ctx.tx[~sfAsset1In]; + auto const asset2In = ctx.tx[~sfAsset2In]; + auto const ePrice = ctx.tx[~sfEPrice]; + auto const lpTokens = ctx.tx[~sfLPToken]; + // Valid options are: + // LPTokens + // Asset1In + // Asset1In and Asset2In + // Asset1In and LPTokens + // Asset1In and EPrice + if ((asset1In && asset2In && (lpTokens || ePrice)) || + (asset1In && lpTokens && (asset2In || ePrice)) || + (asset1In && ePrice && (asset2In || lpTokens)) || + (ePrice && !asset1In) || (asset2In && !asset1In) || + (!lpTokens && !asset1In)) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid combination of " + "deposit fields."; + return temBAD_AMM_OPTIONS; + } + + if (lpTokens && *lpTokens <= beast::zero) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid LPTokens"; + return temBAD_AMM_TOKENS; + } + + if (auto const res = invalidAmount(asset1In, (lpTokens || ePrice))) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid Asset1In"; + return *res; + } + + if (auto const res = invalidAmount(asset2In)) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid Asset2InAmount"; + return *res; + } + + if (auto const res = invalidAmount(ePrice)) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid EPrice"; + return *res; + } + + return preflight2(ctx); +} + +TER +AMMDeposit::preclaim(PreclaimContext const& ctx) +{ + auto const accountID = ctx.tx[sfAccount]; + + if (!ctx.view.read(keylet::account(accountID))) + { + JLOG(ctx.j.debug()) << "AMM Deposit: Invalid account."; + return terNO_ACCOUNT; + } + + auto const ammSle = getAMMSle(ctx.view, ctx.tx[sfAMMID]); + if (!ammSle) + { + JLOG(ctx.j.debug()) << "AMM Deposit: Invalid AMMID."; + return terNO_ACCOUNT; + } + + auto const asset1In = ctx.tx[~sfAsset1In]; + auto const asset2Out = ctx.tx[~sfAsset2Out]; + + if ((asset1In && requireAuth(ctx.view, asset1In->issue(), accountID)) || + (asset2Out && requireAuth(ctx.view, asset2Out->issue(), accountID))) + { + JLOG(ctx.j.debug()) << "AMM Instance: account is not authorized"; + return tecNO_PERMISSION; + } + + if (isFrozen(ctx.view, asset1In) || isFrozen(ctx.view, asset2Out)) + { + JLOG(ctx.j.debug()) << "AMM Deposit involves frozen asset."; + return tecFROZEN; + } + + auto const [asset1, asset2, lptAMMBalance] = + ammHolds(ctx.view, *ammSle, std::nullopt, std::nullopt, ctx.j); + if (asset1 <= beast::zero || asset2 <= beast::zero || + lptAMMBalance <= beast::zero) + { + JLOG(ctx.j.debug()) + << "AMM Deposit: reserves or tokens balance is zero."; + return tecAMM_BALANCE; + } + + if (auto const lpTokens = ctx.tx[~sfLPToken]; + lpTokens && lpTokens->issue() != lptAMMBalance.issue()) + { + JLOG(ctx.j.debug()) << "AMM Deposit: invalid LPTokens."; + return temBAD_AMM_TOKENS; + } + + return tesSUCCESS; +} + +void +AMMDeposit::preCompute() +{ + return Transactor::preCompute(); +} + +std::pair +AMMDeposit::applyGuts(Sandbox& sb) +{ + auto const asset1In = ctx_.tx[~sfAsset1In]; + auto const asset2In = ctx_.tx[~sfAsset2In]; + auto const ePrice = ctx_.tx[~sfEPrice]; + auto const lpTokensDeposit = ctx_.tx[~sfLPToken]; + auto ammSle = getAMMSle(sb, ctx_.tx[sfAMMID]); + assert(ammSle); + auto const ammAccountID = ammSle->getAccountID(sfAMMAccount); + + auto const tfee = getTradingFee(*ammSle, account_); + + auto const [asset1, asset2, lptAMMBalance] = ammHolds( + sb, + *ammSle, + asset1In ? asset1In->issue() : std::optional{}, + asset2In ? asset2In->issue() : std::optional{}, + ctx_.journal); + + auto const [result, depositedTokens] = + [&, + asset1 = std::ref(asset1), + asset2 = std::ref(asset2), + lptAMMBalance = + std::ref(lptAMMBalance)]() -> std::pair { + if (asset1In) + { + if (asset2In) + return equalDepositLimit( + sb, + ammAccountID, + asset1, + asset2, + lptAMMBalance, + *asset1In, + *asset2In); + else if (lpTokensDeposit) + return singleDepositTokens( + sb, + ammAccountID, + asset1, + lptAMMBalance, + *lpTokensDeposit, + tfee); + else if (ePrice) + return singleDepositEPrice( + sb, + ammAccountID, + asset1, + *asset1In, + lptAMMBalance, + *ePrice, + tfee); + else + return singleDeposit( + sb, ammAccountID, asset1, lptAMMBalance, *asset1In, tfee); + } + else if (lpTokensDeposit) + return equalDepositTokens( + sb, + ammAccountID, + asset1, + asset2, + lptAMMBalance, + *lpTokensDeposit); + // should not happen. + JLOG(j_.error()) << "AMM Deposit: invalid options."; + return std::make_pair(tecAMM_FAILED_DEPOSIT, STAmount{}); + }(); + + if (result == tesSUCCESS && depositedTokens != beast::zero) + { + ammSle->setFieldAmount( + sfLPTokenBalance, lptAMMBalance + depositedTokens); + sb.update(ammSle); + } + + return {result, result == tesSUCCESS}; +} + +TER +AMMDeposit::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + // This is a ledger with just the fees paid and any unfunded or expired + // offers we encounter removed. It's used when handling Fill-or-Kill offers, + // if the order isn't going to be placed, to avoid wasting the work we did. + Sandbox sbCancel(&ctx_.view()); + + auto const result = applyGuts(sb); + if (result.second) + sb.apply(ctx_.rawView()); + else + sbCancel.apply(ctx_.rawView()); + + return result.first; +} + +std::pair +AMMDeposit::deposit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Deposit, + std::optional const& asset2Deposit, + STAmount const& lpTokensDeposit) +{ + // Check account has sufficient funds + auto balance = [&](auto const& asset) { + return accountHolds( + view, + account_, + asset.issue().currency, + asset.issue().account, + FreezeHandling::fhZERO_IF_FROZEN, + ctx_.journal) >= asset; + }; + + // Deposit asset1Deposit + if (!balance(asset1Deposit)) + { + JLOG(ctx_.journal.debug()) + << "AMM Deposit: account has insufficient balance to deposit " + << asset1Deposit; + return {tecUNFUNDED_AMM, STAmount{}}; + } + auto res = ammSend(view, account_, ammAccount, asset1Deposit, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) + << "AMM Deposit: failed to deposit " << asset1Deposit; + return {res, STAmount{}}; + } + + // Deposit asset2Deposit + if (asset2Deposit) + { + if (!balance(*asset2Deposit)) + { + JLOG(ctx_.journal.debug()) + << "AMM Deposit: account has insufficient balance to deposit " + << *asset2Deposit; + return {tecUNFUNDED_AMM, STAmount{}}; + } + res = ammSend(view, account_, ammAccount, *asset2Deposit, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) + << "AMM Deposit: failed to deposit " << *asset2Deposit; + return {res, STAmount{}}; + } + } + + // Deposit LP tokens + res = + accountSend(view, ammAccount, account_, lpTokensDeposit, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) << "AMM Deposit: failed to deposit LPTokens"; + return {res, STAmount{}}; + } + + return {tesSUCCESS, lpTokensDeposit}; +} + +/** Proportional deposit of pools assets in exchange for the specified + * amount of LPTokens. + */ +std::pair +AMMDeposit::equalDepositTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit) +{ + auto const frac = + divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + return deposit( + view, + ammAccount, + multiply(asset1Balance, frac, asset1Balance.issue()), + multiply(asset2Balance, frac, asset2Balance.issue()), + lpTokensDeposit); +} + +/** Proportional deposit of pool assets with the constraints on the maximum + * amount of each asset that the trader is willing to deposit. + * a = (t/T) * A (1) + * b = (t/T) * B (2) + * where + * A,B: current pool composition + * T: current balance of outstanding LPTokens + * a: balance of asset A being added + * b: balance of asset B being added + * t: balance of LPTokens issued to LP after a successful transaction + * Use equation 1 to compute the amount of , given the amount in Asset1In. + * Let this be Z + * Use equation 2 to compute the amount of asset2, given t~Z. Let + * the computed amount of asset2 be X. + * If X <= amount in Asset2In: + * The amount of asset1 to be deposited is the one specified in Asset1In + * The amount of asset2 to be deposited is X + * The amount of LPTokens to be issued is Z + * If X > amount in Asset2In: + * Use equation 2 to compute , given the amount in Asset2In. Let this be W + * Use equation 1 to compute the amount of asset1, given t~W from above. + * Let the computed amount of asset1 be Y + * If Y <= amount in Asset1In: + * The amount of asset1 to be deposited is Y + * The amount of asset2 to be deposited is the one specified in Asset2In + * The amount of LPTokens to be issued is W + * else, failed transaction + */ +std::pair +AMMDeposit::equalDepositLimit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1In, + STAmount const& asset2In) +{ + auto frac = Number{asset1In} / asset1Balance; + auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + if (tokens == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + auto const asset2Deposit = asset2Balance * frac; + if (asset2Deposit <= asset2In) + return deposit( + view, + ammAccount, + asset1In, + toSTAmount(asset2Balance.issue(), asset2Deposit), + tokens); + frac = Number{asset2In} / asset2Balance; + tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + if (tokens == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + auto const asset1Deposit = asset1Balance * frac; + if (asset1Deposit <= asset1In) + return deposit( + view, + ammAccount, + toSTAmount(asset1Balance.issue(), asset1Deposit), + asset2In, + tokens); + return {tecAMM_FAILED_DEPOSIT, STAmount{}}; +} + +/** Single asset deposit of the amount of asset specified by Asset1In. + * t = T * (sqrt(1 + (b - 0.5 * tfee * b) / B) - 1) (3) + * Use equation 3 to compute amount of LPTokens to be issued, given + * the amount in Asset1In. + */ +std::pair +AMMDeposit::singleDeposit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1In, + std::uint16_t tfee) +{ + auto const tokens = + calcLPTokensIn(asset1Balance, asset1In, lptAMMBalance, tfee); + if (tokens == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + return deposit(view, ammAccount, asset1In, std::nullopt, tokens); +} + +/** Single asset asset1 is deposited to obtain some share of + * the AMM instance's pools represented by amount of LPTokens. + * b = (((t/T + 1)**2 - 1) / (1 - 0.5 * tfee)) * B (4) + * Use equation 4 to compute the amount of asset1 to be deposited, + * given t represented by amount of LPTokens. + */ +std::pair +AMMDeposit::singleDepositTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit, + std::uint16_t tfee) +{ + auto const asset1Deposit = + calcAssetIn(asset1Balance, lpTokensDeposit, lptAMMBalance, tfee); + return deposit( + view, ammAccount, asset1Deposit, std::nullopt, lpTokensDeposit); +} + +/** Single asset deposit with two constraints. + * a. Amount of asset1 if specified in Asset1In specifies the maximum + * amount of asset1 that the trader is willing to deposit. + * b. The effective-price of the LPToken traded out does not exceed + * the specified 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) + * Use equation 3 to compute the amount of LPTokens out, given the amount + * of Asset1In. Let this be X. + * Use equation III to compute the effective-price of the trade given + * Asset1In amount as the asset in and the LPTokens amount X as asset out. + * Let this be Y. + * If Y <= amount in EPrice: + * The amount of asset1 to be deposited is given by amount in Asset1In + * The amount of LPTokens to be issued is X + * If (Y>EPrice) OR (amount in Asset1In does not exist): + * Use equations 3 & III and the given EPrice to compute the following + * two variables: + * The amount of asset1 in. Let this be Q + * The amount of LPTokens out. Let this be W + * The amount of asset1 to be deposited is Q + * The amount of LPTokens to be issued is W + */ +std::pair +AMMDeposit::singleDepositEPrice( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset1In, + STAmount const& lptAMMBalance, + STAmount const& ePrice, + std::uint16_t tfee) +{ + if (asset1In != beast::zero) + { + auto const tokens = + calcLPTokensIn(asset1Balance, asset1In, lptAMMBalance, tfee); + if (tokens == beast::zero) + return {tecAMM_FAILED_DEPOSIT, STAmount{}}; + auto const ep = Number{asset1In} / tokens; + if (ep <= ePrice) + return deposit(view, ammAccount, asset1In, std::nullopt, tokens); + } + + auto const asset1In_ = toSTAmount( + asset1Balance.issue(), + power(ePrice * lptAMMBalance, 2) * feeMultHalf(tfee) / asset1Balance - + 2 * ePrice * lptAMMBalance); + auto const tokens = toSTAmount(lptAMMBalance.issue(), asset1In_ / ePrice); + return deposit(view, ammAccount, asset1In_, std::nullopt, tokens); +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/tx/impl/AMMDeposit.h b/src/ripple/app/tx/impl/AMMDeposit.h new file mode 100644 index 00000000000..e5e61c16d16 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMDeposit.h @@ -0,0 +1,214 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMDEPOSIT_H_INCLUDED +#define RIPPLE_TX_AMMDEPOSIT_H_INCLUDED + +#include + +namespace ripple { + +class Sandbox; + +/** AMMDeposit implements AMM deposit Transactor. + * The deposit transaction is used to add liquidity to the AMM instance pool, + * thus obtaining some share of the instance's pools in the form of LPTokens. + * If the trader deposits proportional values of both assets without changing + * their relative price, then no trading fee is charged on the transaction. + * The trader can specify different combination of the fields in the deposit. + * LPTokens - transaction assumes proportional deposit of pools assets in + * exchange for the specified amount of LPTokens of the AMM instance. + * Asset1In - transaction assumes single asset deposit of the amount of asset + * specified by Asset1In. This is essentially an equal asset deposit + * and a swap. + * Asset1In and Asset2In - transaction assumes proportional deposit of pool + * assets with the constraints on the maximum amount of each asset that + * the trader is willing to deposit. + * Asset1In and LPTokens - transaction assumes that a single asset asset1 + * is deposited to obtain some share of the AMM instance's pools + * represented by amount of LPTokens. + * Asset1In and EPrice - transaction assumes single asset deposit with + * the following two constraints: + * a. amount of asset1 if specified in Asset1In specifies the maximum + * amount of asset1 that the trader is willing to deposit + * b. The effective-price of the LPTokens traded out does not exceed + * the specified EPrice. + * Following updates after a successful AMMDeposit transaction: + * The deposited asset, if XRP, is transferred from the account that initiated + * the transaction to the AMM instance account, thus changing the Balance + * field of each account. + * The deposited asset, if tokens, are balanced between the AMM account + * and the issuer account trustline. + * The LPTokens are issued by the AMM instance account to the account + * that initiated the transaction and a new trustline is created, + * if there does not exist one. + * The pool composition is updated. + */ +class AMMDeposit : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit AMMDeposit(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static NotTEC + preflight(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Gather information beyond what the Transactor base class gathers. */ + void + preCompute() override; + + /** Attempt to create the AMM instance. */ + TER + doApply() override; + +private: + std::pair + applyGuts(Sandbox& view); + + /** Deposit requested assets and token amount into LP account. + * @param view + * @param ammAccount AMM account + * @param asset1Deposit deposit amount + * @param asset2Deposit deposit amount + * @param lpTokensDeposit amount of tokens to deposit + * @return + */ + std::pair + deposit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Deposit, + std::optional const& asset2Deposit, + STAmount const& lpTokensDeposit); + + /** Equal asset deposit (LPTokens) for the specified share of + * the AMM instance pools. The trading fee is not charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param asset2Balance current AMM asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param lpTokensDeposit amount of tokens to deposit + * @return + */ + std::pair + equalDepositTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit); + + /** Equal asset deposit (Asset1In, Asset2In) with the constraint on + * the maximum amount of both assets that the trader is willing to deposit. + * The trading fee is not charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param asset2Balance current AMM asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1In maximum asset1 deposit amount + * @param asset2In maximum asset2 deposit amount + * @return + */ + std::pair + equalDepositLimit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1In, + STAmount const& asset2In); + + /** Single asset deposit (Asset1In) by the amount. + * The trading fee is charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1In requested asset1 deposit amount + * @param tfee trading fee in basis points + * @return + */ + std::pair + singleDeposit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1In, + std::uint16_t tfee); + + /** Single asset deposit (Asset1In, LPTokens) by the tokens. + * The trading fee is charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param lptAMMBalance current AMM LPT balance + * @param lpTokensDeposit amount of tokens to deposit + * @param tfee trading fee in basis points + * @return + */ + std::pair + singleDepositTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit, + std::uint16_t tfee); + + /** Single asset deposit (Asset1In, EPrice) with two constraints. + * The trading fee is charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param asset1In requested asset1 deposit amount + * @param lptAMMBalance current AMM LPT balance + * @param ePrice maximum effective price + * @param tfee + * @return + */ + std::pair + singleDepositEPrice( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset1In, + STAmount const& lptAMMBalance, + STAmount const& ePrice, + std::uint16_t tfee); +}; + +} // namespace ripple + +#endif // RIPPLE_TX_AMMDEPOSIT_H_INCLUDED diff --git a/src/ripple/app/tx/impl/AMMVote.cpp b/src/ripple/app/tx/impl/AMMVote.cpp new file mode 100644 index 00000000000..ef1bf1517c0 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMVote.cpp @@ -0,0 +1,217 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +AMMVote::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx}; +} + +NotTEC +AMMVote::preflight(PreflightContext const& ctx) +{ + if (!ammRequiredAmendments(ctx.rules)) + return temDISABLED; + + auto const ret = preflight1(ctx); + if (!isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfUniversalMask) + { + JLOG(ctx.j.debug()) << "AMM Vote: invalid flags."; + return temINVALID_FLAG; + } + + if (ctx.tx[sfFeeVal] > 65000) + { + JLOG(ctx.j.debug()) << "AMM Vote: invalid trading fee."; + return temBAD_FEE; + } + + return preflight2(ctx); +} + +TER +AMMVote::preclaim(PreclaimContext const& ctx) +{ + if (!ctx.view.read(keylet::account(ctx.tx[sfAccount]))) + { + JLOG(ctx.j.debug()) << "AMM Vote: Invalid account."; + return terNO_ACCOUNT; + } + + if (!getAMMSle(ctx.view, ctx.tx[sfAMMID])) + { + JLOG(ctx.j.debug()) << "AMM Vote: Invalid AMM account."; + return terNO_ACCOUNT; + } + + return tesSUCCESS; +} + +void +AMMVote::preCompute() +{ + return Transactor::preCompute(); +} + +std::pair +AMMVote::applyGuts(Sandbox& sb) +{ + auto const feeNew = ctx_.tx[sfFeeVal]; + auto const amm = getAMMSle(sb, ctx_.tx[sfAMMID]); + assert(amm); + auto const ammAccount = amm->getAccountID(sfAMMAccount); + auto const lptAMMBalance = amm->getFieldAmount(sfLPTokenBalance); + auto const lpTokensNew = lpHolds(sb, ammAccount, account_, ctx_.journal); + if (lpTokensNew == beast::zero) + { + JLOG(ctx_.journal.debug()) << "AMM Vote: account is not LP."; + return {tecAMM_INVALID_TOKENS, false}; + } + + std::optional minTokens{}; + std::size_t minPos{0}; + STArray updatedVoteSlots; + Number num{0}; + Number den{0}; + // Account already has vote entry + bool foundAccount = false; + // Iterate over the current vote entries and update each entry + // per current total tokens balance and each LP tokens balance. + // Find the entry with the least tokens and whether the account + // has the vote entry. + for (auto const& entry : amm->getFieldArray(sfVoteSlots)) + { + auto const account = entry.getAccountID(sfAccount); + auto lpTokens = lpHolds(sb, ammAccount, account, ctx_.journal); + if (lpTokens == beast::zero) + { + JLOG(j_.debug()) + << "AMMVote::applyGuts, account " << account << " is not LP"; + continue; + } + auto feeVal = entry.getFieldU32(sfFeeVal); + STObject newEntry{sfVoteEntry}; + // The account already has the vote entry. + if (account == account_) + { + lpTokens = lpTokensNew; + feeVal = feeNew; + foundAccount = true; + } + // Keep running numerator/denominator to calculate the updated fee. + num += feeVal * lpTokens; + den += lpTokens; + newEntry.setAccountID(sfAccount, account); + newEntry.setFieldU32(sfFeeVal, feeVal); + newEntry.setFieldU32( + sfVoteWeight, + (std::int64_t)( + Number(lpTokens) * 100000 / lptAMMBalance + Number(1) / 2)); + // Find an entry with the least tokens. + if (!minTokens || lpTokens < *minTokens) + { + minTokens = lpTokens; + minPos = updatedVoteSlots.size(); + } + updatedVoteSlots.emplace_back(newEntry); + } + + // The account doesn't have the vote entry. + if (!foundAccount) + { + auto update = [&]() { + STObject newEntry{sfVoteEntry}; + newEntry.setFieldU32(sfFeeVal, feeNew); + newEntry.setFieldU32( + sfVoteWeight, + (std::int64_t)( + Number(lpTokensNew) * 100000 / lptAMMBalance + + Number(1) / 2)); + newEntry.setAccountID(sfAccount, account_); + num += feeNew * lpTokensNew; + den += lpTokensNew; + updatedVoteSlots.emplace_back(newEntry); + }; + // Add new entry if the number of the vote entries + // is less than 8. + if (updatedVoteSlots.size() < 8) + update(); + // Add the entry if the account has more tokens than + // the least token holder. + else if (lpTokensNew > *minTokens) + { + auto const entry = updatedVoteSlots.begin() + minPos; + // Remove the least token vote entry. + num -= entry->getFieldU32(sfFeeVal) * *minTokens; + den -= *minTokens; + updatedVoteSlots.erase(updatedVoteSlots.begin() + minPos); + update(); + } + // All slots are full and the account does not hold more LPTokens + else + { + JLOG(j_.debug()) << "AMMVote::applyGuts, insufficient tokens to " + "override other votes"; + return {tecAMM_FAILED_VOTE, false}; + } + } + + // Update the vote entries and the trading fee. + amm->setFieldArray(sfVoteSlots, updatedVoteSlots); + amm->setFieldU16(sfTradingFee, (std::int64_t)(num / den + Number(1) / 2)); + sb.update(amm); + + return {tesSUCCESS, true}; +} + +TER +AMMVote::doApply() +{ + // This is the ledger view that we work against. Transactions are applied + // as we go on processing transactions. + Sandbox sb(&ctx_.view()); + + // This is a ledger with just the fees paid and any unfunded or expired + // offers we encounter removed. It's used when handling Fill-or-Kill offers, + // if the order isn't going to be placed, to avoid wasting the work we did. + Sandbox sbCancel(&ctx_.view()); + + auto const result = applyGuts(sb); + if (result.second) + sb.apply(ctx_.rawView()); + else + sbCancel.apply(ctx_.rawView()); + + return result.first; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/AMMVote.h b/src/ripple/app/tx/impl/AMMVote.h new file mode 100644 index 00000000000..415bd342137 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMVote.h @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMVOTE_H_INCLUDED +#define RIPPLE_TX_AMMVOTE_H_INCLUDED + +#include + +namespace ripple { + +class Sandbox; + +/** AMMVote implements AMM vote Transactor. + * This transactor allows for the TradingFee of the AMM instance be a votable + * parameter. Any account (LP) that holds the corresponding LPTokens can cast + * a vote using the new AMMVote transaction. VoteSlots array in ltAMM object + * keeps track of upto eight active votes (VoteEntry) for the instance. + * VoteEntry contains: + * Account - account id that cast the vote. + * FeeVal - proposed fee in basis points. + * VoteWeight - LPTokens owned by the account in basis points. + * TradingFee is calculated as sum(VoteWeight_i * fee_i)/sum(VoteWeight_i). + * Every time AMMVote transaction is submitted, the transactor + * - Fails the transaction is the account doesn't hold LPTokens + * - Removes VoteEntry for accounts that don't hold LPTokens + * - If there are fewer than eight VoteEntry objects then add new VoteEntry + * object for the account. + * - If all eight VoteEntry slots are full, then remove VoteEntry that + * holds less LPTokens than the account. If all accounts hold more + * LPTokens then fail transaction. + * - If the account already holds a vote, then update VoteEntry. + * - Calculate and update TradingFee. + */ +class AMMVote : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit AMMVote(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static NotTEC + preflight(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Gather information beyond what the Transactor base class gathers. */ + void + preCompute() override; + + /** Attempt to create the AMM instance. */ + TER + doApply() override; + +private: + std::pair + applyGuts(Sandbox& view); +}; + +} // namespace ripple + +#endif // RIPPLE_TX_AMMVOTE_H_INCLUDED diff --git a/src/ripple/app/tx/impl/AMMWithdraw.cpp b/src/ripple/app/tx/impl/AMMWithdraw.cpp new file mode 100644 index 00000000000..f39053c7406 --- /dev/null +++ b/src/ripple/app/tx/impl/AMMWithdraw.cpp @@ -0,0 +1,566 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2022 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 +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +TxConsequences +AMMWithdraw::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx}; +} + +NotTEC +AMMWithdraw::preflight(PreflightContext const& ctx) +{ + if (!ammRequiredAmendments(ctx.rules)) + return temDISABLED; + + auto const ret = preflight1(ctx); + if (!isTesSuccess(ret)) + return ret; + + auto const uFlags = ctx.tx.getFlags(); + if (uFlags & tfAMMWithdrawMask) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid flags."; + return temINVALID_FLAG; + } + bool const withdrawAll = uFlags & tfAMMWithdrawAll; + + auto const asset1Out = ctx.tx[~sfAsset1Out]; + auto const asset2Out = ctx.tx[~sfAsset2Out]; + auto const ePrice = ctx.tx[~sfEPrice]; + auto const lpTokens = ctx.tx[~sfLPToken]; + // Valid combinations are: + // LPTokens|tfAMMWithdrawAll + // Asset1Out + // Asset1Out and Asset2Out + // Asset1Out and [LPTokens|tfAMMWithdrawAll] + // Asset1Out and EPrice + if ((asset1Out && asset2Out && (lpTokens || withdrawAll || ePrice)) || + (asset1Out && (lpTokens || withdrawAll) && (asset2Out || ePrice)) || + (asset1Out && ePrice && (asset2Out || lpTokens || withdrawAll)) || + (asset2Out && !asset1Out) || (ePrice && !asset1Out) || + (!asset1Out && !lpTokens && !withdrawAll) || + (lpTokens && withdrawAll) || + (asset1Out && withdrawAll && *asset1Out != beast::zero)) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid combination of " + "withdraw fields."; + return temBAD_AMM_OPTIONS; + } + + if (lpTokens && *lpTokens == beast::zero) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens."; + return temBAD_AMM_TOKENS; + } + + if (auto const res = + invalidAmount(asset1Out, withdrawAll || lpTokens || ePrice)) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid Asset1Out"; + return *res; + } + + if (auto const res = invalidAmount(asset2Out)) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid Asset2OutAmount"; + return *res; + } + + if (auto const res = invalidAmount(ePrice)) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid EPrice"; + return *res; + } + + return preflight2(ctx); +} + +TER +AMMWithdraw::preclaim(PreclaimContext const& ctx) +{ + auto const accountID = ctx.tx[sfAccount]; + if (!ctx.view.read(keylet::account(accountID))) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid account."; + return terNO_ACCOUNT; + } + + auto const ammSle = getAMMSle(ctx.view, ctx.tx[sfAMMID]); + if (!ammSle) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: Invalid AMM account."; + return terNO_ACCOUNT; + } + + auto const asset1Out = ctx.tx[~sfAsset1Out]; + auto const asset2Out = ctx.tx[~sfAsset2Out]; + auto const ammAccountID = ammSle->getAccountID(sfAMMAccount); + + if ((asset1Out && requireAuth(ctx.view, asset1Out->issue(), accountID)) || + (asset2Out && requireAuth(ctx.view, asset2Out->issue(), accountID))) + { + JLOG(ctx.j.debug()) << "AMM Instance: account is not authorized"; + return tecNO_PERMISSION; + } + + if (isFrozen(ctx.view, asset1Out) || isFrozen(ctx.view, sfAsset2Out)) + { + JLOG(ctx.j.debug()) << "AMM Withdraw involves frozen asset."; + return tecFROZEN; + } + + auto const lptBalance = + lpHolds(ctx.view, ammAccountID, ctx.tx[sfAccount], ctx.j); + auto const lpTokens = getTxLPTokens(ctx.view, ammAccountID, ctx.tx, ctx.j); + + if (lptBalance <= beast::zero) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: tokens balance is zero."; + return tecAMM_BALANCE; + } + + if (lpTokens && *lpTokens > lptBalance) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid tokens."; + return tecAMM_INVALID_TOKENS; + } + + if (lpTokens && lpTokens->issue() != lptBalance.issue()) + { + JLOG(ctx.j.debug()) << "AMM Withdraw: invalid LPTokens."; + return temBAD_AMM_TOKENS; + } + + return tesSUCCESS; +} + +void +AMMWithdraw::preCompute() +{ + return Transactor::preCompute(); +} + +std::pair +AMMWithdraw::applyGuts(Sandbox& sb) +{ + auto const asset1Out = ctx_.tx[~sfAsset1Out]; + auto const asset2Out = ctx_.tx[~sfAsset2Out]; + auto const ePrice = ctx_.tx[~sfEPrice]; + auto ammSle = getAMMSle(sb, ctx_.tx[sfAMMID]); + assert(ammSle); + auto const ammAccountID = ammSle->getAccountID(sfAMMAccount); + auto const lpTokensWithdraw = + getTxLPTokens(ctx_.view(), ammAccountID, ctx_.tx, ctx_.journal); + + auto const tfee = getTradingFee(*ammSle, account_); + + auto const [asset1, asset2, lptAMMBalance] = ammHolds( + sb, + *ammSle, + asset1Out ? asset1Out->issue() : std::optional{}, + asset2Out ? asset2Out->issue() : std::optional{}, + ctx_.journal); + + auto const [result, withdrawnTokens] = + [&, + asset1 = std::ref(asset1), + asset2 = std::ref(asset2), + lptAMMBalance = + std::ref(lptAMMBalance)]() -> std::pair { + if (asset1Out) + { + if (asset2Out) + return equalWithdrawalLimit( + sb, + ammAccountID, + asset1, + asset2, + lptAMMBalance, + *asset1Out, + *asset2Out); + else if (lpTokensWithdraw) + return singleWithdrawalTokens( + sb, + ammAccountID, + asset1, + lptAMMBalance, + *asset1Out, + *lpTokensWithdraw, + tfee); + else if (ePrice) + return singleWithdrawalEPrice( + sb, + ammAccountID, + asset1, + lptAMMBalance, + *asset1Out, + *ePrice, + tfee); + else + return singleWithdrawal( + sb, ammAccountID, asset1, lptAMMBalance, *asset1Out, tfee); + } + else if (lpTokensWithdraw) + return equalWithdrawalTokens( + sb, + ammAccountID, + asset1, + asset2, + lptAMMBalance, + *lpTokensWithdraw); + // should not happen. + JLOG(j_.error()) << "AMM Withdraw: invalid options."; + return std::make_pair(tecAMM_FAILED_WITHDRAW, STAmount{}); + }(); + + if (result == tesSUCCESS && withdrawnTokens != beast::zero) + { + ammSle->setFieldAmount( + sfLPTokenBalance, lptAMMBalance - withdrawnTokens); + sb.update(ammSle); + } + + return {result, result == tesSUCCESS}; +} + +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()); + + // This is a ledger with just the fees paid and any unfunded or expired + // offers we encounter removed. It's used when handling Fill-or-Kill offers, + // if the order isn't going to be placed, to avoid wasting the work we did. + Sandbox sbCancel(&ctx_.view()); + + auto const result = applyGuts(sb); + if (result.second) + sb.apply(ctx_.rawView()); + else + sbCancel.apply(ctx_.rawView()); + + return result.first; +} + +TER +AMMWithdraw::deleteAccount(Sandbox& view, AccountID const& ammAccountID) +{ + auto sleAMMRoot = view.peek(keylet::account(ammAccountID)); + assert(sleAMMRoot); + auto sleAMM = view.peek(keylet::amm(ctx_.tx[sfAMMID])); + assert(sleAMM); + + if (!sleAMMRoot || !sleAMM) + return tefBAD_LEDGER; + + // Note, the AMM trust lines are deleted since the balance + // goes to 0. It also means there are no linked + // ledger objects. + view.erase(sleAMM); + view.erase(sleAMMRoot); + + return tesSUCCESS; +} + +std::pair +AMMWithdraw::withdraw( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Withdraw, + std::optional const& asset2Withdraw, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw) +{ + auto const ammSle = getAMMSle(view, ctx_.tx[sfAMMID]); + assert(ammSle); + auto const lpTokens = lpHolds(view, ammAccount, account_, ctx_.journal); + auto const [issue1, issue2] = getTokensIssue(*ammSle); + auto const [asset1, asset2] = + ammPoolHolds(view, ammAccount, issue1, issue2, j_); + + // Invalid tokens or withdrawing more than own. + if (lpTokensWithdraw == beast::zero || lpTokensWithdraw > lpTokens) + { + JLOG(ctx_.journal.debug()) + << "AMM Withdraw: failed to withdraw, invalid LP tokens " + << " tokens: " << lpTokensWithdraw << " " << lpTokens; + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // Withdrawing all tokens but balances are not 0. + if (lpTokensWithdraw == lptAMMBalance && asset1Withdraw != asset1 && + (!asset2Withdraw || *asset2Withdraw != asset2)) + { + JLOG(ctx_.journal.debug()) + << "AMM Withdraw: failed to withdraw, invalid LP balance " + << " asset1: " << asset1 << " " << asset1Withdraw + << " asset2: " << asset2 + << (asset2Withdraw ? to_string(*asset2Withdraw) : ""); + return {tecAMM_BALANCE, STAmount{}}; + } + + // Withdraw asset1Withdraw + auto res = + ammSend(view, ammAccount, account_, asset1Withdraw, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) + << "AMM Withdraw: failed to withdraw " << asset1Withdraw; + return {res, STAmount{}}; + } + + // Withdraw asset2Withdraw + if (asset2Withdraw) + { + res = + ammSend(view, ammAccount, account_, *asset2Withdraw, ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) + << "AMM Withdraw: failed to withdraw " << *asset2Withdraw; + return {res, STAmount{}}; + } + } + + // Withdraw LP tokens + res = redeemIOU( + view, + account_, + lpTokensWithdraw, + lpTokensWithdraw.issue(), + ctx_.journal); + if (res != tesSUCCESS) + { + JLOG(ctx_.journal.debug()) + << "AMM Withdraw: failed to withdraw LPTokens"; + return {res, STAmount{}}; + } + + if (lpTokensWithdraw == lptAMMBalance) + return {deleteAccount(view, ammAccount), STAmount{}}; + + return {tesSUCCESS, lpTokensWithdraw}; +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ +std::pair +AMMWithdraw::equalWithdrawalTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw) +{ + auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); + return withdraw( + view, + ammAccount, + multiply(asset1Balance, frac, asset1Balance.issue()), + multiply(asset2Balance, frac, asset2Balance.issue()), + lptAMMBalance, + lpTokensWithdraw); +} + +/** 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 added + * b: balance of asset B being added + * t: balance of LPTokens issued to LP after a successful transaction + * Use equation 5 to compute , given the amount in Asset1Out. Let this be Z + * Use equation 6 to compute the amount of asset2, given t~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 , given the amount in Asset2Out. Let this be Q + * Use equation 6 to compute the amount of asset1, given t~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 +AMMWithdraw::equalWithdrawalLimit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& asset2Out) +{ + auto frac = Number{asset1Out} / asset1Balance; + auto const asset2Withdraw = asset2Balance * frac; + if (asset2Withdraw <= asset2Out) + return withdraw( + view, + ammAccount, + asset1Out, + toSTAmount(asset2Out.issue(), asset2Withdraw), + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac)); + frac = Number{asset2Out} / asset2Balance; + auto const asset1Withdraw = asset1Balance * frac; + return withdraw( + view, + ammAccount, + toSTAmount(asset1Out.issue(), asset1Withdraw), + asset2Out, + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac)); +} + +/** Withdrawal of single asset equivalent to the amount specified in Asset1Out. + * t = T * (1 - sqrt(1 - b/(B * (1 - 0.5 * tfee)))) (7) + * Use equation 7 to compute the t, given the amount in Asset1Out. + */ +std::pair +AMMWithdraw::singleWithdrawal( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + std::uint16_t tfee) +{ + auto const tokens = + calcLPTokensOut(asset1Balance, asset1Out, lptAMMBalance, tfee); + if (tokens == beast::zero) + return {tecAMM_FAILED_WITHDRAW, STAmount{}}; + return withdraw( + view, ammAccount, asset1Out, std::nullopt, lptAMMBalance, tokens); +} + +/** withdrawal of single asset specified in Asset1Out proportional + * to the share represented by the amount of LPTokens. + * Y = B * (1 - (1 - t/T)**2) * (1 - 0.5 * tfee) (8) + * 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 + */ +std::pair +AMMWithdraw::singleWithdrawalTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee) +{ + auto const asset1Withdraw = calcWithdrawalByTokens( + asset1Balance, lptAMMBalance, lpTokensWithdraw, tfee); + if (asset1Out == beast::zero || asset1Withdraw >= asset1Out) + return withdraw( + view, + ammAccount, + toSTAmount(asset1Out.issue(), asset1Withdraw), + std::nullopt, + lptAMMBalance, + lpTokensWithdraw); + return {tecAMM_FAILED_WITHDRAW, STAmount{}}; +} + +/** Withdrawal of single asset with two constraints. + * a. amount of asset1 if specified 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 * (1 - (1 - t/T)**2) * (1 - 0.5 * tfee) (8) + * 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 +AMMWithdraw::singleWithdrawalEPrice( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& ePrice, + std::uint16_t tfee) +{ + auto const tokens = lptAMMBalance * + (Number(2) - + lptAMMBalance / (asset1Balance * ePrice * feeMultHalf(tfee))); + if (tokens <= 0) + return {tecAMM_FAILED_WITHDRAW, STAmount{}}; + auto const asset1Out_ = toSTAmount(asset1Out.issue(), tokens / ePrice); + if (asset1Out == beast::zero || + (asset1Out != beast::zero && asset1Out_ >= asset1Out)) + return withdraw( + view, + ammAccount, + asset1Out_, + std::nullopt, + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), tokens)); + + return {tecAMM_FAILED_WITHDRAW, STAmount{}}; +} + +std::optional +AMMWithdraw::getTxLPTokens( + ReadView const& view, + AccountID const& ammAccount, + STTx const& tx, + beast::Journal const journal) +{ + // withdraw all tokens - get the balance + if (tx.getFlags() & tfAMMWithdrawAll) + return lpHolds(view, ammAccount, tx[sfAccount], journal); + else + return tx[~sfLPToken]; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/tx/impl/AMMWithdraw.h b/src/ripple/app/tx/impl/AMMWithdraw.h new file mode 100644 index 00000000000..dcad2545a0d --- /dev/null +++ b/src/ripple/app/tx/impl/AMMWithdraw.h @@ -0,0 +1,238 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012-2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_AMMWITHDRAW_H_INCLUDED +#define RIPPLE_TX_AMMWITHDRAW_H_INCLUDED + +#include + +namespace ripple { + +class Sandbox; + +/** AMMWithdraw implements AMM withdraw Transactor. + * The withdraw transaction is used to remove liquidity from the AMM instance + * pool, thus redeeming some share of the pools that one owns in the form + * of LPTokens. If the trader withdraws proportional values of both assets + * without changing their relative pricing, no trading fee is charged on + * the transaction. The trader can specify different combination of + * the fields in the withdrawal. + * LPTokens - transaction assumes proportional withdrawal of pool assets + * for the amount of LPTokens. + * Asset1Out - transaction assumes withdrawal of single asset equivalent + * to the amount specified in Asset1Out. + * Asset1Out and Asset2Out - transaction assumes all assets withdrawal + * with the constraints on the maximum amount of each asset that + * the trader is willing to withdraw. + * Asset1Out and LPTokens - transaction assumes withdrawal of single + * asset specified in Asset1Out proportional to the share represented + * by the amount of LPTokens. + * Asset1Out and EPrice - transaction assumes withdrawal of single + * asset with the following constraints: + * a. Amount of asset1 if specified 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. + * Following updates after a successful transaction: + * The withdrawn asset, if XRP, is transferred from AMM instance account + * to the account that initiated the transaction, thus changing + * the Balance field of each account. + * The withdrawn asset, if token, is balanced between the AMM instance + * account and the issuer account. + * The LPTokens ~ are balanced between the AMM instance account and + * the account that initiated the transaction. + * The pool composition is updated. + */ +class AMMWithdraw : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + explicit AMMWithdraw(ApplyContext& ctx) : Transactor(ctx) + { + } + + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static NotTEC + preflight(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Gather information beyond what the Transactor base class gathers. */ + void + preCompute() override; + + /** Attempt to create the AMM instance. */ + TER + doApply() override; + +private: + std::pair + applyGuts(Sandbox& view); + + /** Withdraw requested assets and token from AMM into LP account. + * @param view + * @param ammAccount AMM account + * @param asset1Withdraw withdraw amount + * @param asset2Withdraw withdraw amount + * @param lptAMMBalance AMM LPT balance + * @param lpTokensWithdraw LPT withdraw amount + * @return + */ + std::pair + withdraw( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Withdraw, + std::optional const& asset2Withdraw, + STAmount const& lpAMMBalance, + STAmount const& lpTokensWithdraw); + + /** Equal-asset withdrawal (LPTokens) of some AMM instance pools + * shares represented by the number of LPTokens . + * The trading fee is not charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current LP asset1 balance + * @param asset2Balance current LP asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param lpTokensWithdraw amount of tokens to withdraw + * @return + */ + std::pair + equalWithdrawalTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw); + + /** Withdraw both assets (Asset1Out, Asset2Out) with the constraints + * on the maximum amount of each asset that the trader is willing + * to withdraw. The trading fee is not charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param asset2Balance current AMM asset2 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1Out asset1 withdraw amount + * @param asset2Out max asset2 withdraw amount + * @return + */ + std::pair + equalWithdrawalLimit( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& asset2Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& asset2Out); + + /** Single asset withdrawal (Asset1Out) equivalent to the amount specified + * in Asset1Out. The trading fee is charged. + * @param ctx + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1Out asset1 withdraw amount + * @param tfee trading fee in basis points + * @return + */ + std::pair + singleWithdrawal( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + std::uint16_t tfee); + + /** Single asset withdrawal (Asset1Out, LPTokens) proportional + * to the share specified by tokens. The trading fee is charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1Out asset1 withdraw amount + * @param lpTokensWithdraw amount of tokens to withdraw + * @param tfee trading fee in basis points + * @return + */ + std::pair + singleWithdrawalTokens( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& lpTokensWithdraw, + std::uint16_t tfee); + + /** Withdrawal of single asset (Asset1Out, EPrice) with two constraints. + * The trading fee is charged. + * @param view + * @param ammAccount AMM account + * @param asset1Balance current AMM asset1 balance + * @param lptAMMBalance current AMM LPT balance + * @param asset1Out asset1 withdraw amount + * @param ePrice maximum asset1 effective price + * @param tfee trading fee in basis points + * @return + */ + std::pair + singleWithdrawalEPrice( + Sandbox& view, + AccountID const& ammAccount, + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& asset1Out, + STAmount const& ePrice, + std::uint16_t tfee); + + /** Delete AMM account. + * @param view + * @param ammAccountID + * @return + */ + TER + deleteAccount(Sandbox& view, AccountID const& ammAccountID); + + /** Get transaction's LP Tokens. If tfAMMWithdrawAll flag is et + * then return all LP Tokens of LP. + */ + static std::optional + getTxLPTokens( + ReadView const& view, + AccountID const& ammAccount, + STTx const& tx, + beast::Journal const journal); +}; + +} // namespace ripple + +#endif // RIPPLE_TX_AMMWITHDRAW_H_INCLUDED diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 8664c6492b9..2a0a986b4d2 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -135,12 +135,16 @@ XRPNotCreated::visitEntry( bool XRPNotCreated::finalize( - STTx const&, + STTx const& tx, TER const, XRPAmount const fee, ReadView const&, beast::Journal const& j) { + // AMM is created with AMMInstanceCreate, not payment + if (tx.getTxnType() == ttAMM_INSTANCE_CREATE) + return true; + // The net change should never be positive, as this would mean that the // transaction created XRP out of thin air. That's not possible. if (drops_ > 0) @@ -320,7 +324,13 @@ AccountRootsNotDeleted::finalize( ReadView const&, beast::Journal const& j) { - if (tx.getTxnType() == ttACCOUNT_DELETE && result == tesSUCCESS) + // AMM account root can be deleted as the result of AMM withdraw + // transaction when the total AMM LP Tokens balance goes to 0. + // Not every AMM withdraw deletes the AMM account, accountsDeleted_ + // is set if it is deleted. + if ((tx.getTxnType() == ttACCOUNT_DELETE || + (tx.getTxnType() == ttAMM_WITHDRAW && accountsDeleted_ == 1)) && + result == tesSUCCESS) { if (accountsDeleted_ == 1) return true; @@ -372,6 +382,7 @@ LedgerEntryTypesMatch::visitEntry( case ltNEGATIVE_UNL: case ltNFTOKEN_PAGE: case ltNFTOKEN_OFFER: + case ltAMM: break; default: invalidTypeAdded_ = true; @@ -472,7 +483,9 @@ ValidNewAccountRoot::finalize( } // From this point on we know exactly one account was created. - if (tx.getTxnType() == ttPAYMENT && result == tesSUCCESS) + if ((tx.getTxnType() == ttPAYMENT || + tx.getTxnType() == ttAMM_INSTANCE_CREATE) && + result == tesSUCCESS) { std::uint32_t const startingSeq{ view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1}; diff --git a/src/ripple/app/tx/impl/Payment.cpp b/src/ripple/app/tx/impl/Payment.cpp index ccb0f1935a9..a064ba69a61 100644 --- a/src/ripple/app/tx/impl/Payment.cpp +++ b/src/ripple/app/tx/impl/Payment.cpp @@ -269,6 +269,14 @@ Payment::preclaim(PreclaimContext const& ctx) return tecDST_TAG_NEEDED; } + else if (sleDst->getFlags() & lsfAMM) + { + // Paying directly into the AMM pool is invalid. + JLOG(ctx.j.trace()) + << "Malformed transaction: Direct payment into AMM is invalid."; + + return tecAMM_DIRECT_PAYMENT; + } if (paths || sendMax || !saDstAmount.native()) { diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 581a700cf75..f9e7d884678 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -18,6 +18,11 @@ //============================================================================== #include +#include +#include +#include +#include +#include #include #include #include @@ -147,6 +152,16 @@ invoke_preflight(PreflightContext const& ctx) return invoke_preflight_helper(ctx); case ttNFTOKEN_ACCEPT_OFFER: return invoke_preflight_helper(ctx); + case ttAMM_INSTANCE_CREATE: + return invoke_preflight_helper(ctx); + case ttAMM_DEPOSIT: + return invoke_preflight_helper(ctx); + case ttAMM_WITHDRAW: + return invoke_preflight_helper(ctx); + case ttAMM_VOTE: + return invoke_preflight_helper(ctx); + case ttAMM_BID: + return invoke_preflight_helper(ctx); default: assert(false); return {temUNKNOWN, TxConsequences{temUNKNOWN}}; @@ -248,6 +263,16 @@ invoke_preclaim(PreclaimContext const& ctx) return invoke_preclaim(ctx); case ttNFTOKEN_ACCEPT_OFFER: return invoke_preclaim(ctx); + case ttAMM_INSTANCE_CREATE: + return invoke_preclaim(ctx); + case ttAMM_DEPOSIT: + return invoke_preclaim(ctx); + case ttAMM_WITHDRAW: + return invoke_preclaim(ctx); + case ttAMM_VOTE: + return invoke_preclaim(ctx); + case ttAMM_BID: + return invoke_preclaim(ctx); default: assert(false); return temUNKNOWN; @@ -311,6 +336,16 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return NFTokenCancelOffer::calculateBaseFee(view, tx); case ttNFTOKEN_ACCEPT_OFFER: return NFTokenAcceptOffer::calculateBaseFee(view, tx); + case ttAMM_INSTANCE_CREATE: + return AMMCreate::calculateBaseFee(view, tx); + case ttAMM_DEPOSIT: + return AMMDeposit::calculateBaseFee(view, tx); + case ttAMM_WITHDRAW: + return AMMWithdraw::calculateBaseFee(view, tx); + case ttAMM_VOTE: + return AMMVote::calculateBaseFee(view, tx); + case ttAMM_BID: + return AMMBid::calculateBaseFee(view, tx); default: assert(false); return FeeUnit64{0}; @@ -463,6 +498,26 @@ invoke_apply(ApplyContext& ctx) NFTokenAcceptOffer p(ctx); return p(); } + case ttAMM_INSTANCE_CREATE: { + AMMCreate p(ctx); + return p(); + } + case ttAMM_DEPOSIT: { + AMMDeposit p(ctx); + return p(); + } + case ttAMM_WITHDRAW: { + AMMWithdraw p(ctx); + return p(); + } + case ttAMM_VOTE: { + AMMVote p(ctx); + return p(); + } + case ttAMM_BID: { + AMMBid p(ctx); + return p(); + } default: assert(false); return {temUNKNOWN, false}; diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 54e78ecc991..f8b4719c75e 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -348,9 +348,15 @@ xrpLiquid( auto const balance = view.balanceHook(id, xrpAccount(), fullBalance); - STAmount amount = balance - reserve; - if (balance < reserve) - amount.clear(); + STAmount amount = [&]() { + // AMM doesn't require the reserves + if (sle->getFlags() & lsfAMM) + return balance; + STAmount amount = balance - reserve; + if (balance < reserve) + amount.clear(); + return amount; + }(); JLOG(j.trace()) << "accountHolds:" << " account=" << to_string(id) diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index eb4906f3af7..bb6ae1eefbd 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1239,6 +1239,7 @@ class RPCParser {"account_objects", &RPCParser::parseAccountItems, 1, 5}, {"account_offers", &RPCParser::parseAccountItems, 1, 4}, {"account_tx", &RPCParser::parseAccountTransactions, 1, 8}, + {"amm_info", &RPCParser::parseAsIs, 1, 2}, {"book_changes", &RPCParser::parseLedgerId, 1, 1}, {"book_offers", &RPCParser::parseBookOffers, 2, 7}, {"can_delete", &RPCParser::parseCanDelete, 0, 1}, diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index d841d8035fc..3f35b1349cb 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 52; +static constexpr std::size_t numFeatures = 53; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -338,6 +338,7 @@ extern uint256 const fixNFTokenDirV1; extern uint256 const fixNFTokenNegOffer; extern uint256 const featureNonFungibleTokensV1_1; extern uint256 const fixTrustLinesToSelf; +extern uint256 const featureAMM; extern uint256 const fixUniversalNumber; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index f7f35355ee4..0ef2f5c1244 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -263,6 +263,10 @@ nft_buys(uint256 const& id) noexcept; Keylet nft_sells(uint256 const& id) noexcept; +/** AMM entry */ +Keylet +amm(uint256 const& amm) noexcept; + } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/src/ripple/protocol/LedgerFormats.h b/src/ripple/protocol/LedgerFormats.h index 2dd04b1264b..d0811cabcde 100644 --- a/src/ripple/protocol/LedgerFormats.h +++ b/src/ripple/protocol/LedgerFormats.h @@ -161,6 +161,12 @@ enum LedgerEntryType : std::uint16_t */ ltNFTOKEN_OFFER = 0x0037, + /** The ledger object which tracks the AMM. + + \sa keylet::amm + */ + ltAMM = 0x0079, + //--------------------------------------------------------------------------- /** A special type, matching any ledger entry type. @@ -232,6 +238,7 @@ enum LedgerSpecificFlags { lsfDefaultRipple = 0x00800000, // True, trust lines allow rippling by default lsfDepositAuth = 0x01000000, // True, all deposits require authorization + lsfAMM = 0x02000000, // True, AMM account // ltOFFER lsfPassive = 0x00010000, diff --git a/src/ripple/protocol/QualityFunction.h b/src/ripple/protocol/QualityFunction.h new file mode 100644 index 00000000000..385bb192870 --- /dev/null +++ b/src/ripple/protocol/QualityFunction.h @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_PROTOCOL_QUALITYFUNCTION_H_INCLUDED +#define RIPPLE_PROTOCOL_QUALITYFUNCTION_H_INCLUDED + +#include + +namespace ripple { + +class Quality; + +/** Average Quality as a function of out: q(out) = m * out + b, + * where m = -1 / poolGets, b = poolPays / poolGets. Used + * to find required output amount when quality limit is + * provided for one path optimization. + */ +class QualityFunction +{ +private: + Number m_; // slope + Number b_; // intercept + +public: + QualityFunction(Quality const& quality); + QualityFunction(Amounts const& amounts); + QualityFunction(); + //~QualityFunction() = default; + + /** Combines QF with the next step QF + */ + void + combineWithNext(QualityFunction const& qf); + + /** Find output to produce the requested + * instant quality (Spot Price Quality). + * @param quality requested instant quality (quality limit) + */ + std::optional + outFromInstQ(Quality const& quality); + + /** Return true if the quality function is constant + */ + bool + isConst() const + { + return m_ == 0; + } +}; + +} // namespace ripple + +#endif // RIPPLE_PROTOCOL_QUALITYFUNCTION_H_INCLUDED diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 5039e4e0524..266c5515cdf 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -345,6 +345,7 @@ extern SF_UINT16 const sfLedgerEntryType; extern SF_UINT16 const sfTransactionType; extern SF_UINT16 const sfSignerWeight; extern SF_UINT16 const sfTransferFee; +extern SF_UINT16 const sfTradingFee; // 16-bit integers (uncommon) extern SF_UINT16 const sfVersion; @@ -367,6 +368,8 @@ extern SF_UINT32 const sfTransferRate; extern SF_UINT32 const sfWalletSize; extern SF_UINT32 const sfOwnerCount; extern SF_UINT32 const sfDestinationTag; +extern SF_UINT32 const sfTimeStamp; +extern SF_UINT32 const sfDiscountedFee; // 32-bit integers (uncommon) extern SF_UINT32 const sfHighQualityIn; @@ -400,6 +403,8 @@ extern SF_UINT32 const sfMintedNFTokens; extern SF_UINT32 const sfBurnedNFTokens; extern SF_UINT32 const sfHookStateCount; extern SF_UINT32 const sfEmitGeneration; +extern SF_UINT32 const sfFeeVal; +extern SF_UINT32 const sfVoteWeight; // 64-bit integers (common) extern SF_UINT64 const sfIndexNext; @@ -430,6 +435,8 @@ extern SF_UINT160 const sfTakerPaysCurrency; extern SF_UINT160 const sfTakerPaysIssuer; extern SF_UINT160 const sfTakerGetsCurrency; extern SF_UINT160 const sfTakerGetsIssuer; +extern SF_UINT160 const sfTokenCurrency; +extern SF_UINT160 const sfTokenIssuer; // 256-bit (common) extern SF_UINT256 const sfLedgerHash; @@ -445,6 +452,7 @@ extern SF_UINT256 const sfNFTokenID; extern SF_UINT256 const sfEmitParentTxnID; extern SF_UINT256 const sfEmitNonce; extern SF_UINT256 const sfEmitHookHash; +extern SF_UINT256 const sfAMMID; // 256-bit (uncommon) extern SF_UINT256 const sfBookDirectory; @@ -476,12 +484,24 @@ extern SF_AMOUNT const sfHighLimit; extern SF_AMOUNT const sfFee; extern SF_AMOUNT const sfSendMax; extern SF_AMOUNT const sfDeliverMin; +extern SF_AMOUNT const sfAsset1; +extern SF_AMOUNT const sfAsset2; +extern SF_AMOUNT const sfAsset1In; +extern SF_AMOUNT const sfAsset2In; +extern SF_AMOUNT const sfAsset1Out; +extern SF_AMOUNT const sfAsset2Out; +extern SF_AMOUNT const sfEPrice; +extern SF_AMOUNT const sfMinSlotPrice; +extern SF_AMOUNT const sfMaxSlotPrice; +extern SF_AMOUNT const sfPrice; +extern SF_AMOUNT const sfLPTokenBalance; // currency amount (uncommon) extern SF_AMOUNT const sfMinimumOffer; extern SF_AMOUNT const sfRippleEscrow; extern SF_AMOUNT const sfDeliveredAmount; extern SF_AMOUNT const sfNFTokenBrokerFee; +extern SF_AMOUNT const sfLPToken; // variable length (common) extern SF_VL const sfPublicKey; @@ -521,6 +541,7 @@ extern SF_ACCOUNT const sfUnauthorize; extern SF_ACCOUNT const sfRegularKey; extern SF_ACCOUNT const sfNFTokenMinter; extern SF_ACCOUNT const sfEmitCallback; +extern SF_ACCOUNT const sfAMMAccount; // account (uncommon) extern SF_ACCOUNT const sfHookAccount; @@ -549,6 +570,12 @@ extern SField const sfSignerEntry; extern SField const sfNFToken; extern SField const sfEmitDetails; extern SField const sfHook; +extern SField const sfVoteEntry; +extern SField const sfAuctionSlot; +extern SField const sfAuthAccount; +extern SField const sfAMMToken; +extern SField const sfToken1; +extern SField const sfToken2; extern SField const sfSigner; extern SField const sfMajority; @@ -571,6 +598,8 @@ extern SField const sfAffectedNodes; extern SField const sfMemos; extern SField const sfNFTokens; extern SField const sfHooks; +extern SField const sfVoteSlots; +extern SField const sfAuthAccounts; // array of objects (uncommon) extern SField const sfMajorities; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 38342f0c139..51f40319b18 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -120,6 +120,9 @@ enum TEMcodes : TERUnderlyingType { temSEQ_AND_TICKET, temBAD_NFTOKEN_TRANSFER_FEE, + + temBAD_AMM_OPTIONS, + temBAD_AMM_TOKENS, }; //------------------------------------------------------------------------------ @@ -289,6 +292,15 @@ enum TECcodes : TERUnderlyingType { tecINSUFFICIENT_FUNDS = 159, tecOBJECT_NOT_FOUND = 160, tecINSUFFICIENT_PAYMENT = 161, + tecUNFUNDED_AMM = 162, + tecAMM_BALANCE = 163, + tecAMM_FAILED_DEPOSIT = 164, + tecAMM_FAILED_WITHDRAW = 165, + tecAMM_INVALID_TOKENS = 166, + tecAMM_EXISTS = 167, + tecAMM_FAILED_BID = 168, + tecAMM_DIRECT_PAYMENT = 169, + tecAMM_FAILED_VOTE = 170 }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFlags.h b/src/ripple/protocol/TxFlags.h index 0b907c72235..6f9da47028f 100644 --- a/src/ripple/protocol/TxFlags.h +++ b/src/ripple/protocol/TxFlags.h @@ -134,6 +134,11 @@ constexpr std::uint32_t const tfNFTokenCancelOfferMask = ~(tfUniversal); // NFTokenAcceptOffer flags: constexpr std::uint32_t const tfNFTokenAcceptOfferMask = ~tfUniversal; +// AMM Flags: +constexpr std::uint32_t tfAMMWithdrawAll = 0x00010000; +constexpr std::uint32_t tfAMMWithdrawMask = + ~(tfUniversal | tfAMMWithdrawAll); + // clang-format on } // namespace ripple diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 250c29d69c1..dad0be12907 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -139,6 +139,21 @@ enum TxType : std::uint16_t /** This transaction accepts an existing offer to buy or sell an existing NFT. */ ttNFTOKEN_ACCEPT_OFFER = 29, + /** This transaction type creates an AMM instance */ + ttAMM_INSTANCE_CREATE = 35, + + /** This transaction type deposits into an AMM instance */ + ttAMM_DEPOSIT = 36, + + /** This transaction type withdraws from an AMM instance */ + ttAMM_WITHDRAW = 37, + + /** This transaction type votes for the trading fee */ + ttAMM_VOTE = 38, + + /** This transaction type bids for the auction slot */ + ttAMM_BID = 39, + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 8b0edac7e64..27b78e22d45 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -448,6 +448,7 @@ REGISTER_FIX (fixNFTokenDirV1, Supported::yes, DefaultVote::no) REGISTER_FIX (fixNFTokenNegOffer, Supported::yes, DefaultVote::no); REGISTER_FEATURE(NonFungibleTokensV1_1, Supported::yes, DefaultVote::no); REGISTER_FIX (fixTrustLinesToSelf, Supported::yes, DefaultVote::no); +REGISTER_FEATURE(AMM, Supported::yes, DefaultVote::no); REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::yes); // The following amendments have been active for at least two years. Their diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 69e7cc55d0f..f1803872eee 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -63,6 +63,7 @@ enum class LedgerNameSpace : std::uint16_t { NFTOKEN_OFFER = 'q', NFTOKEN_BUY_OFFERS = 'h', NFTOKEN_SELL_OFFERS = 'i', + AMM = 'A', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -370,6 +371,12 @@ nft_sells(uint256 const& id) noexcept return {ltDIR_NODE, indexHash(LedgerNameSpace::NFTOKEN_SELL_OFFERS, id)}; } +Keylet +amm(uint256 const& amm) noexcept +{ + return {ltAMM, indexHash(LedgerNameSpace::AMM, amm)}; +} + } // namespace keylet } // namespace ripple diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index 1b4aa63c2ba..54566aa17d3 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -59,6 +59,45 @@ InnerObjectFormats::InnerObjectFormats() {sfNFTokenID, soeREQUIRED}, {sfURI, soeOPTIONAL}, }); + + add(sfVoteEntry.jsonName.c_str(), + sfVoteEntry.getCode(), + { + {sfAccount, soeREQUIRED}, + {sfFeeVal, soeREQUIRED}, + {sfVoteWeight, soeREQUIRED}, + }); + + add(sfAuctionSlot.jsonName.c_str(), + sfAuctionSlot.getCode(), + { + {sfAccount, soeREQUIRED}, + {sfTimeStamp, soeREQUIRED}, + {sfDiscountedFee, soeREQUIRED}, + {sfPrice, soeREQUIRED}, + {sfAuthAccounts, soeOPTIONAL}, + }); + + add(sfToken1.jsonName.c_str(), + sfToken1.getCode(), + { + {sfTokenCurrency, soeREQUIRED}, + {sfTokenIssuer, soeREQUIRED}, + }); + + add(sfToken2.jsonName.c_str(), + sfToken2.getCode(), + { + {sfTokenCurrency, soeREQUIRED}, + {sfTokenIssuer, soeREQUIRED}, + }); + + add(sfAMMToken.jsonName.c_str(), + sfAMMToken.getCode(), + { + {sfToken1, soeREQUIRED}, + {sfToken2, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index 7d5cf9d21aa..54595d72c74 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -261,6 +261,19 @@ LedgerFormats::LedgerFormats() {sfPreviousTxnLgrSeq, soeREQUIRED} }, commonFields); + + add(jss::AMM, + ltAMM, + { + {sfAMMAccount, soeREQUIRED}, + {sfTradingFee, soeREQUIRED}, + {sfVoteSlots, soeOPTIONAL}, + {sfAuctionSlot, soeOPTIONAL}, + {sfLPTokenBalance, soeREQUIRED}, + {sfAMMToken, soeREQUIRED} + }, + commonFields); + // clang-format on } diff --git a/src/ripple/protocol/impl/QualityFunction.cpp b/src/ripple/protocol/impl/QualityFunction.cpp new file mode 100644 index 00000000000..184843812c3 --- /dev/null +++ b/src/ripple/protocol/impl/QualityFunction.cpp @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include + +namespace ripple { + +QualityFunction::QualityFunction() : m_(0), b_(0) +{ +} + +QualityFunction::QualityFunction(Quality const& quality) +{ + if (quality.rate() <= beast::zero) + Throw("QualityFunction invalid initialization."); + m_ = 0; + b_ = 1 / quality.rate(); +} + +QualityFunction::QualityFunction(Amounts const& amounts) +{ + if (amounts.in <= beast::zero || amounts.out <= beast::zero) + Throw("QualityFunction invalid initialization."); + m_ = -1 / amounts.in; + b_ = amounts.out / amounts.in; +} + +void +QualityFunction::combineWithNext(QualityFunction const& qf) +{ + if (m_ == 0 && b_ == 0) + { + m_ = qf.m_; + b_ = qf.b_; + } + else + { + m_ += b_ * qf.m_; + b_ *= qf.b_; + } +} + +std::optional +QualityFunction::outFromInstQ(Quality const& quality) +{ + if (m_ != 0 && quality.rate() != beast::zero) + { + auto const out = -(b_ - root(b_ / quality.rate(), 2)) / m_; + if (out <= 0) + return std::nullopt; + return out; + } + return std::nullopt; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 73098319b28..b1eaa5c2817 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -95,6 +95,7 @@ CONSTRUCT_TYPED_SFIELD(sfLedgerEntryType, "LedgerEntryType", UINT16, CONSTRUCT_TYPED_SFIELD(sfTransactionType, "TransactionType", UINT16, 2); CONSTRUCT_TYPED_SFIELD(sfSignerWeight, "SignerWeight", UINT16, 3); CONSTRUCT_TYPED_SFIELD(sfTransferFee, "TransferFee", UINT16, 4); +CONSTRUCT_TYPED_SFIELD(sfTradingFee, "TradingFee", UINT16, 5); // 16-bit integers (uncommon) CONSTRUCT_TYPED_SFIELD(sfVersion, "Version", UINT16, 16); @@ -150,6 +151,10 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32, CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44); CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45); CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46); +CONSTRUCT_TYPED_SFIELD(sfFeeVal, "FeeVal", UINT32, 47); +CONSTRUCT_TYPED_SFIELD(sfVoteWeight, "VoteWeight", UINT32, 48); +CONSTRUCT_TYPED_SFIELD(sfTimeStamp, "TimeStamp", UINT32, 49); +CONSTRUCT_TYPED_SFIELD(sfDiscountedFee, "DiscountedFee", UINT32, 50); // 64-bit integers (common) CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1); @@ -180,6 +185,8 @@ CONSTRUCT_TYPED_SFIELD(sfTakerPaysCurrency, "TakerPaysCurrency", UINT160, CONSTRUCT_TYPED_SFIELD(sfTakerPaysIssuer, "TakerPaysIssuer", UINT160, 2); CONSTRUCT_TYPED_SFIELD(sfTakerGetsCurrency, "TakerGetsCurrency", UINT160, 3); CONSTRUCT_TYPED_SFIELD(sfTakerGetsIssuer, "TakerGetsIssuer", UINT160, 4); +CONSTRUCT_TYPED_SFIELD(sfTokenCurrency, "TokenCurrency", UINT160, 5); +CONSTRUCT_TYPED_SFIELD(sfTokenIssuer, "TokenIssuer", UINT160, 6); // 256-bit (common) CONSTRUCT_TYPED_SFIELD(sfLedgerHash, "LedgerHash", UINT256, 1); @@ -195,6 +202,7 @@ CONSTRUCT_TYPED_SFIELD(sfNFTokenID, "NFTokenID", UINT256, CONSTRUCT_TYPED_SFIELD(sfEmitParentTxnID, "EmitParentTxnID", UINT256, 11); CONSTRUCT_TYPED_SFIELD(sfEmitNonce, "EmitNonce", UINT256, 12); CONSTRUCT_TYPED_SFIELD(sfEmitHookHash, "EmitHookHash", UINT256, 13); +CONSTRUCT_TYPED_SFIELD(sfAMMID, "AMMID", UINT256, 14); // 256-bit (uncommon) CONSTRUCT_TYPED_SFIELD(sfBookDirectory, "BookDirectory", UINT256, 16); @@ -227,12 +235,24 @@ CONSTRUCT_TYPED_SFIELD(sfHighLimit, "HighLimit", AMOUNT, CONSTRUCT_TYPED_SFIELD(sfFee, "Fee", AMOUNT, 8); CONSTRUCT_TYPED_SFIELD(sfSendMax, "SendMax", AMOUNT, 9); CONSTRUCT_TYPED_SFIELD(sfDeliverMin, "DeliverMin", AMOUNT, 10); +CONSTRUCT_TYPED_SFIELD(sfAsset1, "Asset1", AMOUNT, 11); +CONSTRUCT_TYPED_SFIELD(sfAsset2, "Asset2", AMOUNT, 12); +CONSTRUCT_TYPED_SFIELD(sfMinSlotPrice, "MinSlotPrice", AMOUNT, 13); +CONSTRUCT_TYPED_SFIELD(sfMaxSlotPrice, "MaxSlotPrice", AMOUNT, 14); // currency amount (uncommon) CONSTRUCT_TYPED_SFIELD(sfMinimumOffer, "MinimumOffer", AMOUNT, 16); CONSTRUCT_TYPED_SFIELD(sfRippleEscrow, "RippleEscrow", AMOUNT, 17); CONSTRUCT_TYPED_SFIELD(sfDeliveredAmount, "DeliveredAmount", AMOUNT, 18); CONSTRUCT_TYPED_SFIELD(sfNFTokenBrokerFee, "NFTokenBrokerFee", AMOUNT, 19); +CONSTRUCT_TYPED_SFIELD(sfAsset1In, "Asset1In", AMOUNT, 20); +CONSTRUCT_TYPED_SFIELD(sfAsset2In, "Asset2In", AMOUNT, 21); +CONSTRUCT_TYPED_SFIELD(sfAsset1Out, "Asset1Out", AMOUNT, 22); +CONSTRUCT_TYPED_SFIELD(sfAsset2Out, "Asset2Out", AMOUNT, 23); +CONSTRUCT_TYPED_SFIELD(sfLPToken, "LPToken", AMOUNT, 24); +CONSTRUCT_TYPED_SFIELD(sfEPrice, "EPrice", AMOUNT, 25); +CONSTRUCT_TYPED_SFIELD(sfPrice, "Price", AMOUNT, 26); +CONSTRUCT_TYPED_SFIELD(sfLPTokenBalance, "LPTokenBalance", AMOUNT, 27); // variable length (common) CONSTRUCT_TYPED_SFIELD(sfPublicKey, "PublicKey", VL, 1); @@ -273,6 +293,7 @@ CONSTRUCT_TYPED_SFIELD(sfUnauthorize, "Unauthorize", ACCOUNT, CONSTRUCT_TYPED_SFIELD(sfRegularKey, "RegularKey", ACCOUNT, 8); CONSTRUCT_TYPED_SFIELD(sfNFTokenMinter, "NFTokenMinter", ACCOUNT, 9); CONSTRUCT_TYPED_SFIELD(sfEmitCallback, "EmitCallback", ACCOUNT, 10); +CONSTRUCT_TYPED_SFIELD(sfAMMAccount, "AMMAccount", ACCOUNT, 11); // account (uncommon) CONSTRUCT_TYPED_SFIELD(sfHookAccount, "HookAccount", ACCOUNT, 16); @@ -301,6 +322,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfSignerEntry, "SignerEntry", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfNFToken, "NFToken", OBJECT, 12); CONSTRUCT_UNTYPED_SFIELD(sfEmitDetails, "EmitDetails", OBJECT, 13); CONSTRUCT_UNTYPED_SFIELD(sfHook, "Hook", OBJECT, 14); +CONSTRUCT_UNTYPED_SFIELD(sfAMM, "AMM", OBJECT, 15); // inner object (uncommon) CONSTRUCT_UNTYPED_SFIELD(sfSigner, "Signer", OBJECT, 16); @@ -312,6 +334,12 @@ CONSTRUCT_UNTYPED_SFIELD(sfHookExecution, "HookExecution", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfHookDefinition, "HookDefinition", OBJECT, 22); CONSTRUCT_UNTYPED_SFIELD(sfHookParameter, "HookParameter", OBJECT, 23); CONSTRUCT_UNTYPED_SFIELD(sfHookGrant, "HookGrant", OBJECT, 24); +CONSTRUCT_UNTYPED_SFIELD(sfVoteEntry, "VoteEntry", OBJECT, 25); +CONSTRUCT_UNTYPED_SFIELD(sfAuctionSlot, "AuctionSlot", OBJECT, 27); +CONSTRUCT_UNTYPED_SFIELD(sfAuthAccount, "AuthAccount", OBJECT, 28); +CONSTRUCT_UNTYPED_SFIELD(sfAMMToken, "AMMToken", OBJECT, 29); +CONSTRUCT_UNTYPED_SFIELD(sfToken1, "Token1", OBJECT, 30); +CONSTRUCT_UNTYPED_SFIELD(sfToken2, "Token2", OBJECT, 31); // array of objects // ARRAY/1 is reserved for end of array @@ -325,6 +353,9 @@ CONSTRUCT_UNTYPED_SFIELD(sfAffectedNodes, "AffectedNodes", ARRAY, CONSTRUCT_UNTYPED_SFIELD(sfMemos, "Memos", ARRAY, 9); CONSTRUCT_UNTYPED_SFIELD(sfNFTokens, "NFTokens", ARRAY, 10); CONSTRUCT_UNTYPED_SFIELD(sfHooks, "Hooks", ARRAY, 11); +CONSTRUCT_UNTYPED_SFIELD(sfVoteSlots, "VoteSlots", ARRAY, 14); +// TODO STTx fails in testMalformedSerializedForm because 15 is hardcoded in payload2 as unknown field +CONSTRUCT_UNTYPED_SFIELD(sfAuthAccounts, "AuthAccounts", ARRAY, 21); // array of objects (uncommon) CONSTRUCT_UNTYPED_SFIELD(sfMajorities, "Majorities", ARRAY, 16); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index c660b1cea3f..94a3b504005 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -42,6 +42,14 @@ transResults() TERUnderlyingType, std::pair> const results { + MAKE_ERROR(tecAMM_BALANCE, "AMM has invalid balance."), + MAKE_ERROR(tecAMM_INVALID_TOKENS, "AMM invalid LP tokens."), + MAKE_ERROR(tecAMM_FAILED_DEPOSIT, "AMM failed deposit."), + MAKE_ERROR(tecAMM_FAILED_WITHDRAW, "AMM failed withdraw."), + MAKE_ERROR(tecAMM_EXISTS, "AMM instance exists."), + MAKE_ERROR(tecAMM_FAILED_BID, "AMM failed bid."), + MAKE_ERROR(tecAMM_FAILED_VOTE, "AMM failed vote."), + MAKE_ERROR(tecAMM_DIRECT_PAYMENT, "AMM account can not be payment destination."), MAKE_ERROR(tecCLAIM, "Fee claimed. Sequence used. No action."), MAKE_ERROR(tecDIR_FULL, "Can not add entry to full directory."), MAKE_ERROR(tecFAILED_PROCESSING, "Failed to correctly process transaction."), @@ -58,6 +66,7 @@ transResults() MAKE_ERROR(tecOVERSIZE, "Object exceeded serialization limits."), MAKE_ERROR(tecUNFUNDED, "Not enough XRP to satisfy the reserve requirement."), MAKE_ERROR(tecUNFUNDED_ADD, "DEPRECATED."), + MAKE_ERROR(tecUNFUNDED_AMM, "Insufficient balance to fund AMM."), MAKE_ERROR(tecUNFUNDED_OFFER, "Insufficient balance to fund created offer."), MAKE_ERROR(tecUNFUNDED_PAYMENT, "Insufficient XRP balance to send."), MAKE_ERROR(tecOWNERS, "Non-zero owner count."), @@ -126,6 +135,8 @@ transResults() MAKE_ERROR(telCAN_NOT_QUEUE_FULL, "Can not queue at this time: queue is full."), MAKE_ERROR(temMALFORMED, "Malformed transaction."), + MAKE_ERROR(temBAD_AMM_OPTIONS, "Malformed: Invalid combination of options."), + MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."), MAKE_ERROR(temBAD_AMOUNT, "Can only send positive amounts."), MAKE_ERROR(temBAD_CURRENCY, "Malformed: Bad currency."), MAKE_ERROR(temBAD_EXPIRATION, "Malformed: Bad expiration."), diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index ce0d5db921f..98162310bca 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -80,6 +80,60 @@ TxFormats::TxFormats() }, commonFields); + add(jss::AMMInstanceCreate, + ttAMM_INSTANCE_CREATE, + { + {sfAsset1, soeREQUIRED}, + {sfAsset2, soeREQUIRED}, + {sfTradingFee, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, + }, + commonFields); + + add(jss::AMMDeposit, + ttAMM_DEPOSIT, + { + {sfAMMID, soeREQUIRED}, + {sfAsset1In, soeOPTIONAL}, + {sfAsset2In, soeOPTIONAL}, + {sfEPrice, soeOPTIONAL}, + {sfLPToken, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, + }, + commonFields); + + add(jss::AMMWithdraw, + ttAMM_WITHDRAW, + { + {sfAMMID, soeREQUIRED}, + {sfAsset1Out, soeOPTIONAL}, + {sfAsset2Out, soeOPTIONAL}, + {sfEPrice, soeOPTIONAL}, + {sfLPToken, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, + }, + commonFields); + + add(jss::AMMVote, + ttAMM_VOTE, + { + {sfAMMID, soeREQUIRED}, + {sfFeeVal, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, + }, + commonFields); + + add(jss::AMMBid, + ttAMM_BID, + { + {sfAMMID, soeREQUIRED}, + {sfMinSlotPrice, soeOPTIONAL}, + {sfMaxSlotPrice, soeOPTIONAL}, + {sfAuthAccounts, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, + }, + commonFields); + add(jss::OfferCancel, ttOFFER_CANCEL, { diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 1c5bf8463b0..b65e1a597c0 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -47,8 +47,25 @@ JSS(Account); // in: TransactionSign; field. JSS(AccountDelete); // transaction type. JSS(AccountRoot); // ledger type. JSS(AccountSet); // transaction type. +JSS(AMM); // ledger type +JSS(AMMAccount); // field +JSS(AMMBid); // transaction type +JSS(AMMID); // field +JSS(AMMInstanceCreate); // transaction type +JSS(AMMDeposit); // transaction type +JSS(AMMVote); // transaction type +JSS(AMMWithdraw); // transaction type JSS(Amendments); // ledger type. JSS(Amount); // in: TransactionSign; field. +JSS(Asset1); // in/out: AMM IOU/XRP pool amount +JSS(Asset2); // in/out: AMM IOU pool amount +JSS(Asset1In); // in: AMM Deposit option +JSS(Asset2In); // in: AMM Deposit option +JSS(Asset1Out); // in: AMM Withdraw option +JSS(Asset2Out); // in: AMM Withdraw option +JSS(AuctionSlot); // out: AMM Auction Slot +JSS(AuthAccount); // in: AMM Auction Slot +JSS(AuthAccounts); // in: AMM Auction Slot JSS(Check); // ledger type. JSS(CheckCancel); // transaction type. JSS(CheckCash); // transaction type. @@ -57,20 +74,25 @@ JSS(ClearFlag); // field. JSS(DeliverMin); // in: TransactionSign JSS(DepositPreauth); // transaction and ledger type. JSS(Destination); // in: TransactionSign; field. +JSS(DiscountedFee); // out: AMM Auction Slot JSS(DirectoryNode); // ledger type. JSS(EnableAmendment); // transaction type. +JSS(EPrice); // in: AMM Deposit option JSS(Escrow); // ledger type. JSS(EscrowCancel); // transaction type. JSS(EscrowCreate); // transaction type. JSS(EscrowFinish); // transaction type. JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. +JSS(FeeVal); // in: AMM Vote JSS(Flags); // in/out: TransactionSign; field. JSS(incomplete_shards); // out: OverlayImpl, PeerImp JSS(Invalid); // JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LedgerHashes); // ledger type. JSS(LimitAmount); // field. +JSS(MaxSlotPrice); // in: AMM Bid +JSS(MinSlotPrice); // in: AMM Bid JSS(NFTokenBurn); // transaction type. JSS(NFTokenMint); // transaction type. JSS(NFTokenOffer); // ledger type. @@ -78,6 +100,7 @@ JSS(NFTokenAcceptOffer); // transaction type. JSS(NFTokenCancelOffer); // transaction type. JSS(NFTokenCreateOffer); // transaction type. JSS(NFTokenPage); // ledger type. +JSS(LPToken); // in: AMM Liquidity Provider tokens JSS(Offer); // ledger type. JSS(OfferCancel); // transaction type. JSS(OfferCreate); // transaction type. @@ -88,6 +111,7 @@ JSS(Payment); // transaction type. JSS(PaymentChannelClaim); // transaction type. JSS(PaymentChannelCreate); // transaction type. JSS(PaymentChannelFund); // transaction type. +JSS(Price); // out: AMM Auction Slot JSS(RippleState); // ledger type. JSS(SLE_hit_rate); // out: GetCounts. JSS(SetFee); // transaction type. @@ -104,10 +128,14 @@ JSS(TakerGets); // field. JSS(TakerPays); // field. JSS(Ticket); // ledger type. JSS(TicketCreate); // transaction type. +JSS(TimeInterval); // out: AMM Auction Slot JSS(TxnSignature); // field. +JSS(TradingFee); // in/out: AMM trading fee JSS(TransactionType); // in: TransactionSign. JSS(TransferRate); // in: TransferRate. JSS(TrustSet); // transaction type. +JSS(VoteSlots); // out: AMM Vote +JSS(VoteWeight); // out: AMM Vote JSS(aborted); // out: InboundLedger JSS(accepted); // out: LedgerToJson, OwnerInfo, SubmitTransaction JSS(account); // in/out: many @@ -135,11 +163,14 @@ JSS(age); // out: NetworkOPs, Peers JSS(alternatives); // out: PathRequest, RipplePathFind JSS(amendment_blocked); // out: NetworkOPs JSS(amendments); // in: AccountObjects, out: NetworkOPs +JSS(amm_id); // in: AMMID in amm_info JSS(amount); // out: AccountChannels JSS(api_version); // in: many, out: Version JSS(api_version_low); // out: Version JSS(applied); // out: SubmitTransaction JSS(asks); // out: Subscribe +JSS(asset1); // in: Asset1 in amm_info +JSS(asset2); // in: Asset2 in amm_info JSS(assets); // out: GatewayBalances JSS(authorized); // out: AccountLines JSS(auth_change); // out: AccountInfo diff --git a/src/ripple/rpc/handlers/AMMInfo.cpp b/src/ripple/rpc/handlers/AMMInfo.cpp new file mode 100644 index 00000000000..b4869e5d90a --- /dev/null +++ b/src/ripple/rpc/handlers/AMMInfo.cpp @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +std::optional +getAccount(Json::Value const& v, Json::Value& result) +{ + std::string strIdent(v.asString()); + AccountID accountID; + + if (auto jv = RPC::accountFromString(accountID, strIdent)) + { + for (auto it = jv.begin(); it != jv.end(); ++it) + result[it.memberName()] = (*it); + + return std::nullopt; + } + return std::optional(accountID); +} + +Json::Value +doAMMInfo(RPC::JsonContext& context) +{ + auto const& params(context.params); + Json::Value result; + std::optional accountID; + + uint256 ammID{}; + STAmount asset1{noIssue()}; + STAmount asset2{noIssue()}; + if (!params.isMember(jss::amm_id)) + { + // May provide asset1/asset2 as amounts + if (!params.isMember(jss::asset1) || !params.isMember(jss::asset2)) + return RPC::missing_field_error(jss::amm_id); + if (!amountFromJsonNoThrow(asset1, params[jss::asset1]) || + !amountFromJsonNoThrow(asset2, params[jss::asset2])) + { + RPC::inject_error(rpcACT_MALFORMED, result); + return result; + } + ammID = calcAMMGroupHash(asset1.issue(), asset2.issue()); + } + else if (!ammID.parseHex(params[jss::amm_id].asString())) + { + RPC::inject_error(rpcACT_MALFORMED, result); + return result; + } + + std::shared_ptr ledger; + result = RPC::lookupLedger(ledger, context); + if (!ledger) + return result; + + if (params.isMember(jss::account)) + { + accountID = getAccount(params[jss::account], result); + if (!accountID || !ledger->read(keylet::account(*accountID))) + { + RPC::inject_error(rpcACT_MALFORMED, result); + return result; + } + } + + auto const amm = getAMMSle(*ledger, ammID); + if (!amm) + return rpcError(rpcACT_NOT_FOUND); + + auto const [issue1, issue2] = [&]() { + if (asset1.issue() == noIssue()) + return getTokensIssue(*amm); + return std::make_pair(asset1.issue(), asset2.issue()); + }(); + + auto const ammAccountID = amm->getAccountID(sfAMMAccount); + + auto const [asset1Balance, asset2Balance] = + ammPoolHolds(*ledger, ammAccountID, issue1, issue2, context.j); + auto const lptAMMBalance = accountID + ? lpHolds(*ledger, ammAccountID, *accountID, context.j) + : amm->getFieldAmount(sfLPTokenBalance); + + asset1Balance.setJson(result[jss::Asset1]); + asset2Balance.setJson(result[jss::Asset2]); + lptAMMBalance.setJson(result[jss::LPToken]); + result[jss::TradingFee] = amm->getFieldU16(sfTradingFee); + result[jss::AMMAccount] = to_string(ammAccountID); + Json::Value voteSlots(Json::arrayValue); + if (amm->isFieldPresent(sfVoteSlots)) + { + for (auto const& voteEntry : amm->getFieldArray(sfVoteSlots)) + { + Json::Value vote; + vote[jss::FeeVal] = voteEntry.getFieldU32(sfFeeVal); + vote[jss::VoteWeight] = voteEntry.getFieldU32(sfVoteWeight); + voteSlots.append(vote); + } + } + if (voteSlots.size() > 0) + result[jss::VoteSlots] = voteSlots; + if (amm->isFieldPresent(sfAuctionSlot)) + { + auto const& auctionSlot = + static_cast(amm->peekAtField(sfAuctionSlot)); + if (auctionSlot.isFieldPresent(sfAccount)) + { + Json::Value auction; + auction[jss::TimeInterval] = + timeSlot(ledger->info().parentCloseTime, auctionSlot); + auctionSlot.getFieldAmount(sfPrice).setJson(auction[jss::Price]); + auction[jss::DiscountedFee] = + auctionSlot.getFieldU32(sfDiscountedFee); + result[jss::AuctionSlot] = auction; + } + } + if (!params.isMember(jss::amm_id)) + result[jss::AMMID] = to_string(ammID); + + return result; +} + +} // namespace ripple diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 3c00899d734..367e715ce1f 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -41,6 +41,8 @@ doAccountOffers(RPC::JsonContext&); Json::Value doAccountTxJson(RPC::JsonContext&); Json::Value +doAMMInfo(RPC::JsonContext&); +Json::Value doBookOffers(RPC::JsonContext&); Json::Value doBookChanges(RPC::JsonContext&); diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index 17a15eed31b..dd898ee8722 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -71,6 +71,7 @@ Handler const handlerArray[]{ {"account_objects", byRef(&doAccountObjects), Role::USER, NO_CONDITION}, {"account_offers", byRef(&doAccountOffers), Role::USER, NO_CONDITION}, {"account_tx", byRef(&doAccountTxJson), Role::USER, NO_CONDITION}, + {"amm_info", byRef(&doAMMInfo), Role::USER, NO_CONDITION}, {"blacklist", byRef(&doBlackList), Role::ADMIN, NO_CONDITION}, {"book_changes", byRef(&doBookChanges), Role::USER, NO_CONDITION}, {"book_offers", byRef(&doBookOffers), Role::USER, NO_CONDITION}, diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp new file mode 100644 index 00000000000..c33caef7080 --- /dev/null +++ b/src/test/app/AMM_test.cpp @@ -0,0 +1,2735 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { + +#if 0 +static Json::Value +readOffers(jtx::Env& env, AccountID const& acct) +{ + Json::Value jv; + jv[jss::account] = to_string(acct); + return env.rpc("json", "account_offers", to_string(jv)); +} + +static Json::Value +readLines(jtx::Env& env, AccountID const& acctId) +{ + Json::Value jv; + jv[jss::account] = to_string(acctId); + return env.rpc("json", "account_lines", to_string(jv)); +} + +static Json::Value +accountInfo(jtx::Env& env, AccountID const& acctId) +{ + Json::Value jv; + jv[jss::account] = to_string(acctId); + return env.rpc("json", "account_info", to_string(jv)); +} +#endif + +static XRPAmount +txfee(jtx::Env const& env, std::uint16_t n) +{ + return env.current()->fees().base * n; +} + +static bool +expectLine(jtx::Env& env, AccountID const& account, STAmount const& value) +{ + if (auto const sle = env.le(keylet::line(account, value.issue()))) + { + auto amount = sle->getFieldAmount(sfBalance); + amount.setIssuer(value.issue().account); + if (account > value.issue().account) + amount.negate(); + return amount == value; + } + return false; +} + +static bool +expectOffers( + jtx::Env& env, + AccountID const& account, + std::uint16_t size, + std::optional> const& toMatch = std::nullopt) +{ + std::uint16_t cnt = 0; + std::uint16_t matched = 0; + forEachItem( + *env.current(), account, [&](std::shared_ptr const& sle) { + if (!sle) + return false; + if (sle->getType() == ltOFFER) + { + ++cnt; + if (toMatch && + std::find_if( + toMatch->begin(), toMatch->end(), [&](auto const& a) { + return a.in == sle->getFieldAmount(sfTakerPays) && + a.out == sle->getFieldAmount(sfTakerGets); + }) != toMatch->end()) + ++matched; + } + return true; + }); + return size == cnt && (!toMatch || matched == toMatch->size()); +} + +static auto +ledgerEntryRoot(jtx::Env& env, jtx::Account const& acct) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = "current"; + jvParams[jss::account_root] = acct.human(); + return env.rpc("json", "ledger_entry", to_string(jvParams))[jss::result]; +} + +template +static bool +expectLedgerEntryRoot( + jtx::Env& env, + jtx::Account const& acct, + V const& expectedValue) +{ + auto const jrr = ledgerEntryRoot(env, acct); + auto const value = [&]() -> std::string { + if constexpr (std::is_same_v) + return to_string(expectedValue.xrp()); + else if constexpr (std::is_same_v) + return expectedValue.value().getText(); + else if constexpr (std::is_same_v) + return to_string(expectedValue); + else + assert(0); + }(); + return jrr[jss::node][sfBalance.fieldName] == value; +} + +class Test : public beast::unit_test::suite +{ +protected: + enum class Fund { All, Acct, None }; + jtx::Account const gw; + jtx::Account const carol; + jtx::Account const alice; + jtx::Account const bob; + jtx::IOU const USD; + jtx::IOU const EUR; + jtx::IOU const GBP; + jtx::IOU const BTC; + jtx::IOU const BAD; + +public: + Test() + : gw("gateway") + , carol("carol") + , alice("alice") + , bob("bob") + , USD(gw["USD"]) + , EUR(gw["EUR"]) + , GBP(gw["GBP"]) + , BTC(gw["BTC"]) + , BAD(jtx::IOU(gw, badCurrency())) + { + } + +protected: + void + fund( + jtx::Env& env, + jtx::Account const& gw, + std::vector const& accounts, + std::vector const& amts, + Fund how) + { + fund(env, gw, accounts, 30000 * jtx::dropsPerXRP, amts, how); + } + void + fund( + jtx::Env& env, + jtx::Account const& gw, + std::vector const& accounts, + jtx::PrettyAmount const& xrp, + std::vector const& amts = {}, + Fund how = Fund::All) + { + if (how == Fund::All) + env.fund(xrp, gw); + env.close(); + for (auto const& account : accounts) + { + if (how == Fund::All || how == Fund::Acct) + { + env.fund(xrp, account); + env.close(); + } + for (auto const& amt : amts) + { + env.trust(amt + amt, account); + env.close(); + env(pay(gw, account, amt)); + env.close(); + } + } + } + + template + void + testAMM( + F&& cb, + std::optional> const& pool = {}, + std::optional const& lpt = {}, + std::uint32_t fee = 0, + std::optional const& ter = std::nullopt, + std::optional const& features = std::nullopt) + { + using namespace jtx; + auto env = features ? Env{*this, *features} : Env{*this}; + + auto [asset1, asset2] = [&]() -> std::pair { + if (pool) + return *pool; + return {XRP(10000), USD(10000)}; + }(); + + fund( + env, + gw, + {alice, carol}, + {STAmount{asset2.issue(), 30000}}, + Fund::All); + if (!asset1.native()) + fund( + env, + gw, + {alice, carol}, + {STAmount{asset1.issue(), 30000}}, + Fund::None); + auto tokens = [&]() { + if (lpt) + return *lpt; + return IOUAmount{10000000, 0}; + }(); + AMM ammAlice(env, alice, asset1, asset2, false, fee); + BEAST_EXPECT(ammAlice.expectBalances(asset1, asset2, tokens)); + cb(ammAlice, env); + } + + template + void + stats(C const& t, std::string const& msg) + { + auto const sum = std::accumulate(t.begin(), t.end(), 0.0); + auto const avg = sum / static_cast(t.size()); + auto sd = std::accumulate( + t.begin(), t.end(), 0.0, [&](auto const init, auto const r) { + return init + pow((r - avg), 2); + }); + sd = sqrt(sd / t.size()); + std::cout << msg << " exec time: avg " << avg << " " + << " sd " << sd << std::endl; + } +}; + +struct AMM_test : public Test +{ +public: + AMM_test() : Test() + { + } + +private: + void + testInstanceCreate() + { + testcase("Instance Create"); + + using namespace jtx; + + // XRP to IOU + testAMM([&](AMM& ammAlice, Env&) { + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + }); + + // IOU to IOU + testAMM( + [&](AMM& ammAlice, Env&) { + BEAST_EXPECT(ammAlice.expectBalances( + USD(20000), BTC(0.5), IOUAmount{100, 0})); + }, + std::make_pair(USD(20000), BTC(0.5)), + IOUAmount{100, 0}); + + // IOU to IOU + transfer fee + { + Env env{*this}; + fund(env, gw, {alice}, {USD(25000), BTC(0.625)}, Fund::All); + env(rate(gw, 1.25)); + AMM ammAlice(env, alice, USD(20000), BTC(0.5)); + BEAST_EXPECT(ammAlice.expectBalances( + USD(20000), BTC(0.5), IOUAmount{100, 0})); + // Transfer fee is not charged. + BEAST_EXPECT(expectLine(env, alice, USD(5000))); + BEAST_EXPECT(expectLine(env, alice, BTC(0.125))); + } + + // Require authorization is set, account is authorized + { + Env env{*this}; + env.fund(XRP(30000), gw, alice); + env.close(); + env(fset(gw, asfRequireAuth)); + env.close(); + env.trust(USD(30000), alice); + env.close(); + env(trust(gw, alice["USD"](30000)), txflags(tfSetfAuth)); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + } + + // Cleared global freeze + { + Env env{*this}; + env.fund(XRP(30000), gw, alice); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + env.trust(USD(30000), alice); + env.close(); + AMM ammAliceFail( + env, alice, XRP(10000), USD(10000), ter(tecFROZEN)); + env(fclear(gw, asfGlobalFreeze)); + env.close(); + env(pay(gw, alice, USD(10000))); + env.close(); + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + } + } + + void + testInvalidInstance() + { + testcase("Invalid Instance"); + + using namespace jtx; + + // Can't have both XRP tokens + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, alice, XRP(10000), XRP(10000), ter(temBAD_AMM_TOKENS)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Can't have both tokens the same IOU + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, alice, USD(10000), USD(10000), ter(temBAD_AMM_TOKENS)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Can't have zero amounts + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice(env, alice, XRP(0), USD(10000), ter(temBAD_AMOUNT)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Bad currency + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, alice, XRP(10000), BAD(10000), ter(temBAD_CURRENCY)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Insufficient IOU balance + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, alice, XRP(10000), USD(40000), ter(tecUNFUNDED_PAYMENT)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Insufficient XRP balance + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, alice, XRP(40000), USD(10000), ter(tecUNFUNDED_PAYMENT)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Invalid trading fee + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, + alice, + XRP(10000), + USD(10000), + false, + 65001, + std::nullopt, + std::nullopt, + ter(temBAD_FEE)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // AMM already exists + testAMM([&](AMM& ammAlice, Env& env) { + AMM ammCarol( + env, carol, XRP(10000), USD(10000), ter(tecAMM_EXISTS)); + }); + + // Invalid flags + { + Env env{*this}; + fund(env, gw, {alice}, {USD(30000)}, Fund::All); + AMM ammAlice( + env, + alice, + XRP(10000), + USD(10000), + false, + 0, + tfAMMWithdrawAll, + std::nullopt, + ter(temINVALID_FLAG)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Invalid Account + { + Env env{*this}; + Account bad("bad"); + env.memoize(bad); + AMM ammAlice( + env, + bad, + XRP(10000), + USD(10000), + false, + 0, + std::nullopt, + seq(1), + ter(terNO_ACCOUNT)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Require authorization is set + { + Env env{*this}; + env.fund(XRP(30000), gw, alice); + env.close(); + env(fset(gw, asfRequireAuth)); + env.close(); + env(trust(gw, alice["USD"](30000))); + env.close(); + AMM ammAlice( + env, alice, XRP(10000), USD(10000), ter(tecNO_PERMISSION)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + + // Global freeze + { + Env env{*this}; + env.fund(XRP(30000), gw, alice); + env.close(); + env(fset(gw, asfGlobalFreeze)); + env.close(); + env(trust(gw, alice["USD"](30000))); + env.close(); + AMM ammAlice(env, alice, XRP(10000), USD(10000), ter(tecFROZEN)); + BEAST_EXPECT(!ammAlice.ammExists()); + } + } + + void + testInvalidDeposit() + { + testcase("Invalid Deposit"); + + using namespace jtx; + + // Invalid flags + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit( + alice, + 1000000, + std::nullopt, + tfAMMWithdrawAll, + ter(temINVALID_FLAG)); + }); + + // Invalid options + std::vector, + std::optional, + std::optional, + std::optional>> + invalidOptions = { + // tokens, asset1In, asset2in, EPrice + {1000, std::nullopt, USD(100), std::nullopt}, + {1000, std::nullopt, std::nullopt, STAmount{USD, 1, -1}}, + {std::nullopt, std::nullopt, USD(100), STAmount{USD, 1, -1}}, + {std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}}, + {1000, XRP(100), USD(100), std::nullopt}}; + for (auto const& it : invalidOptions) + { + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit( + alice, + std::get<0>(it), + std::get<1>(it), + std::get<2>(it), + std::get<3>(it), + std::nullopt, + std::nullopt, + ter(temBAD_AMM_OPTIONS)); + }); + } + + // Invalid tokens + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit( + alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); + }); + + // Invalid amount value + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit( + alice, + USD(0), + std::nullopt, + std::nullopt, + std::nullopt, + ter(temBAD_AMOUNT)); + }); + + // Bad currency + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit( + alice, + BAD(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(temBAD_CURRENCY)); + }); + + // Invalid Account + testAMM([&](AMM& ammAlice, Env& env) { + Account bad("bad"); + env.memoize(bad); + ammAlice.deposit( + bad, + 1000000, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + seq(1), + ter(terNO_ACCOUNT)); + }); + + // Invalid AMM + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdrawAll(alice); + ammAlice.deposit( + alice, 10000, std::nullopt, std::nullopt, ter(terNO_ACCOUNT)); + }); + + // Frozen asset + testAMM([&](AMM& ammAlice, Env& env) { + env(fset(gw, asfGlobalFreeze)); + ammAlice.deposit( + carol, + USD(100), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecFROZEN)); + }); + + // Frozen asset, balance is not available + testAMM([&](AMM& ammAlice, Env& env) { + env(fset(gw, asfGlobalFreeze)); + ammAlice.deposit( + carol, + 1000000, + std::nullopt, + std::nullopt, + ter(tecAMM_BALANCE)); + }); + + // Insufficient XRP balance + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(XRP(1000), bob); + env.close(); + ammAlice.deposit( + bob, + XRP(1001), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + }); + + // Insufficient USD balance + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(1000)}, Fund::Acct); + env.close(); + ammAlice.deposit( + bob, + USD(1001), + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + }); + + // Insufficient USD balance by tokens + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(1000)}, Fund::Acct); + env.close(); + ammAlice.deposit( + bob, + 10000000, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + }); + + // Insufficient XRP balance by tokens + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(XRP(1000), bob); + env.trust(USD(100000), bob); + env.close(); + env(pay(gw, bob, USD(90000))); + env.close(); + ammAlice.deposit( + bob, + 10000000, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecUNFUNDED_AMM)); + }); + } + + void + testDeposit() + { + testcase("Deposit"); + + using namespace jtx; + + // Equal deposit: 1000000 tokens, 10% of the current pool + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(11000), IOUAmount{11000000, 0})); + }); + + // Equal limit deposit: deposit USD100 and XRP proportionally + // to the pool composition not to exceed 100XRP. If the amount + // exceeds 100XRP then deposit 100XRP and USD proportionally + // to the pool composition not to exceed 100USD. Fail if exceeded. + // Deposit 100USD/100XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(100), XRP(100)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10100), USD(10100), IOUAmount{10100000, 0})); + }); + + // Equal limit deposit. Deposit 100USD/100XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(200), XRP(100)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10100), USD(10100), IOUAmount{10100000, 0})); + }); + + // TODO. Equal limit deposit. Constraint fails. + + // Single deposit: 1000 USD + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(1000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(11000), IOUAmount{1048808848170152, -8})); + }); + + // Single deposit: 1000 XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, XRP(1000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(10000), IOUAmount{1048808848170152, -8})); + }); + + // Single deposit: 100000 tokens worth of USD + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 100000, USD(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10201), IOUAmount{10100000, 0})); + }); + + // Single deposit: 100000 tokens worth of XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 100000, XRP(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10201), USD(10000), IOUAmount{10100000, 0})); + }); + + // Single deposit with EP not exceeding specified: + // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut) + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit( + carol, USD(1000), std::nullopt, STAmount{USD, 1, -1}); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(11000), IOUAmount{1048808848170152, -8})); + }); + + // Single deposit with EP not exceeding specified: + // 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut) + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit( + carol, USD(100), std::nullopt, STAmount{USD, 2004, -6}); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), + STAmount{USD, 1008016, -2}, + IOUAmount{10040000, 0})); + }); + + // Single deposit with EP not exceeding specified: + // 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut) + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit( + carol, USD(0), std::nullopt, STAmount{USD, 2004, -6}); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), + STAmount{USD, 1008016, -2}, + IOUAmount{10040000, 0})); + }); + + // IOU to IOU + transfer fee + { + Env env{*this}; + fund(env, gw, {alice}, {USD(25000), BTC(0.625)}, Fund::All); + env(rate(gw, 1.25)); + AMM ammAlice(env, alice, USD(20000), BTC(0.5)); + BEAST_EXPECT(ammAlice.expectBalances( + USD(20000), BTC(0.5), IOUAmount{100, 0})); + // Transfer fee is not charged. + BEAST_EXPECT(expectLine(env, alice, USD(5000))); + BEAST_EXPECT(expectLine(env, alice, BTC(0.125))); + // LP deposits, doesn't pay transfer fee. + fund(env, gw, {carol}, {USD(2500), BTC(0.0625)}, Fund::Acct); + ammAlice.deposit(carol, 10); + BEAST_EXPECT(ammAlice.expectBalances( + USD(22000), BTC(0.55), IOUAmount{110, 0})); + BEAST_EXPECT(expectLine(env, carol, USD(500))); + BEAST_EXPECT(expectLine(env, carol, BTC(0.0125))); + } + } + + void + testInvalidWithdraw() + { + testcase("Invalid Withdraw"); + + using namespace jtx; + + // Invalid flags + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + 1000000, + std::nullopt, + std::nullopt, + std::nullopt, + tfPartialPayment, + std::nullopt, + ter(temINVALID_FLAG)); + }); + + // Invalid options + std::vector, + std::optional, + std::optional, + std::optional, + std::optional>> + invalidOptions = { + // tokens, asset1Out, asset2Out, EPrice, tfAMMWithdrawAll + {std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt}, + {1000, + std::nullopt, + std::nullopt, + std::nullopt, + tfAMMWithdrawAll}, + {std::nullopt, + std::nullopt, + USD(100), + std::nullopt, + tfAMMWithdrawAll}, + {1000, std::nullopt, USD(100), std::nullopt, std::nullopt}, + {std::nullopt, + std::nullopt, + std::nullopt, + IOUAmount{250, 0}, + tfAMMWithdrawAll}, + {1000, + std::nullopt, + std::nullopt, + IOUAmount{250, 0}, + std::nullopt}, + {std::nullopt, + std::nullopt, + USD(100), + IOUAmount{250, 0}, + std::nullopt}, + {std::nullopt, + XRP(100), + USD(100), + IOUAmount{250, 0}, + std::nullopt}, + {1000, XRP(100), USD(100), std::nullopt, std::nullopt}, + {std::nullopt, + XRP(100), + USD(100), + std::nullopt, + tfAMMWithdrawAll}}; + for (auto const& it : invalidOptions) + { + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + std::get<0>(it), + std::get<1>(it), + std::get<2>(it), + std::get<3>(it), + std::get<4>(it), + std::nullopt, + ter(temBAD_AMM_OPTIONS)); + }); + } + + // Invalid tokens + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS)); + }); + + // Invalid amount value + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, USD(0), std::nullopt, std::nullopt, ter(temBAD_AMOUNT)); + }); + + // Invalid amount/token value, withdraw all tokens from one side + // of the pool. + { + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + USD(10000), + std::nullopt, + std::nullopt, + ter(tecAMM_FAILED_WITHDRAW)); + }); + + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + XRP(10000), + std::nullopt, + std::nullopt, + ter(tecAMM_FAILED_WITHDRAW)); + }); + + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + std::nullopt, + USD(0), + std::nullopt, + std::nullopt, + tfAMMWithdrawAll, + std::nullopt, + ter(tecAMM_BALANCE)); + }); + } + + // Bad currency + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + alice, + BAD(100), + std::nullopt, + std::nullopt, + ter(temBAD_CURRENCY)); + }); + + // Invalid Account + testAMM([&](AMM& ammAlice, Env& env) { + Account bad("bad"); + env.memoize(bad); + ammAlice.withdraw( + bad, + 1000000, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + seq(1), + ter(terNO_ACCOUNT)); + }); + + // Invalid AMM + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdrawAll(alice); + ammAlice.withdraw( + alice, 10000, std::nullopt, std::nullopt, ter(terNO_ACCOUNT)); + }); + + // Frozen asset + testAMM([&](AMM& ammAlice, Env& env) { + env(fset(gw, asfGlobalFreeze)); + env.close(); + ammAlice.withdraw( + carol, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN)); + }); + + // Frozen asset, balance is not available + testAMM([&](AMM& ammAlice, Env& env) { + env(fset(gw, asfGlobalFreeze)); + env.close(); + ammAlice.withdraw( + carol, 1000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); + }); + + // Carol is not a Liquidity Provider + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw( + carol, 10000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + }); + + // Carol withdraws more than she owns + testAMM([&](AMM& ammAlice, Env&) { + // Single deposit of 100000 worth of tokens, + // which is 10% of the pool. Carol is LP now. + ammAlice.deposit(carol, 1000000); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(11000), IOUAmount{11000000, 0})); + + ammAlice.withdraw( + carol, + 2000000, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(11000), IOUAmount{11000000, 0})); + }); + + // Withdraw with EPrice limit. Fails to withdraw, calculated tokens + // to withdraw are 0. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdraw( + carol, + USD(100), + std::nullopt, + IOUAmount{500, 0}, + ter(tecAMM_FAILED_WITHDRAW)); + }); + + // Withdraw with EPrice limit. Fails to withdraw, calculated tokens + // to withdraw are greater than the LP shares. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdraw( + carol, + USD(100), + std::nullopt, + IOUAmount{600, 0}, + ter(tecAMM_INVALID_TOKENS)); + }); + + // Withdraw with EPrice limit. Fails to withdraw, amount1 + // to withdraw is less than 1700USD. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdraw( + carol, + USD(1700), + std::nullopt, + IOUAmount{520, 0}, + ter(tecAMM_FAILED_WITHDRAW)); + }); + + // Single deposit/withdrawal 1000USD. Fails due to round-off error, + // tokens to withdraw exceeds the LP tokens balance. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(10000)); + ammAlice.withdraw( + carol, + USD(10000), + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }); + } + + void + testWithdraw() + { + testcase("Withdraw"); + + using namespace jtx; + + // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current + // pool + testAMM([&](AMM& ammAlice, Env&) { + // Single deposit of 100000 worth of tokens, + // which is 10% of the pool. Carol is LP now. + ammAlice.deposit(carol, 1000000); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(11000), IOUAmount{11000000, 0})); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1000000, 0})); + + // Carol withdraws all tokens + ammAlice.withdraw(carol, 1000000); + BEAST_EXPECT( + ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); + }); + + // Equal withdrawal by tokens 1000000, 10% + // of the current pool + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw(alice, 1000000); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9000), USD(9000), IOUAmount{9000000, 0})); + }); + + // Equal withdrawal with a limit. Withdraw XRP200. + // If proportional withdraw of USD is less than 100 + // the withdraw that amount, otherwise withdraw USD100 + // and proportionally withdraw XRP. It's the latter + // in this case - XRP100/USD100. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw(alice, XRP(200), USD(100)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9900), USD(9900), IOUAmount{9900000, 0})); + }); + + // Equal withdrawal with a limit. XRP100/USD100. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw(alice, XRP(100), USD(200)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9900), USD(9900), IOUAmount{9900000, 0})); + }); + + // Single withdrawal by amount XRP1000 + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw(alice, XRP(1000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9000), USD(10000), IOUAmount{948683298050514, -8})); + }); + + // Single withdrawal by tokens 10000. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.withdraw(alice, 10000, USD(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(9980.01), IOUAmount{9990000, 0})); + }); + + // Withdraw all tokens. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + + // Can create AMM for the XRP/USD pair + AMM ammCarol(env, carol, XRP(10000), USD(10000)); + BEAST_EXPECT(ammCarol.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + }); + + // Single deposit 1000USD, withdraw all tokens in USD + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, USD(1000)); + ammAlice.withdrawAll(carol, USD(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), + STAmount{USD, UINT64_C(999999999999999), -11}, + IOUAmount{10000000, 0})); + BEAST_EXPECT( + ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero()))); + }); + + // Single deposit 1000USD, withdraw all tokens in XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(1000)); + ammAlice.withdrawAll(carol, XRP(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(9090909091), USD(11000), IOUAmount{10000000, 0})); + }); + + // Single deposit/withdrawal 1000USD + // There is a round-off error. There remains + // a dust amount of tokens + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(1000)); + ammAlice.withdraw(carol, USD(1000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{1000000000000001, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{63, -10})); + }); + + // Single deposit by different accounts and then withdraw + // in reverse. There is a round-off error. There remains + // a dust amount of tokens. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, USD(1000)); + ammAlice.deposit(alice, USD(1000)); + ammAlice.withdraw(alice, USD(1000)); + ammAlice.withdraw(carol, USD(1000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{1000000000000001, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{63, -10})); + }); + + // Equal deposit 10%, withdraw all tokens + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdrawAll(carol); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + }); + + // Equal deposit 10%, withdraw all tokens in USD + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdrawAll(carol, USD(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), + STAmount{USD, UINT64_C(9090909090909092), -12}, + IOUAmount{10000000, 0})); + }); + + // Equal deposit 10%, withdraw all tokens in XRP + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdrawAll(carol, XRP(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(9090909091), USD(11000), IOUAmount{10000000, 0})); + }); + + // Withdraw with EPrice limit. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0}); + BEAST_EXPECT( + ammAlice.expectBalances( + XRPAmount(11000000000), + STAmount{USD, UINT64_C(9372781065088756), -12}, + IOUAmount{1015384615384615, -8}) && + ammAlice.expectLPTokens(carol, IOUAmount{153846153846153, -9})); + }); + + // Withdraw with EPrice limit. AssetOut is 0. + testAMM([&](AMM& ammAlice, Env&) { + ammAlice.deposit(carol, 1000000); + ammAlice.withdraw(carol, USD(0), std::nullopt, IOUAmount{520, 0}); + BEAST_EXPECT( + ammAlice.expectBalances( + XRPAmount(11000000000), + STAmount{USD, UINT64_C(9372781065088756), -12}, + IOUAmount{1015384615384615, -8}) && + ammAlice.expectLPTokens(carol, IOUAmount{153846153846153, -9})); + }); + + // TODO there should be a limit on a single withdrawal amount. + // For instance, in 10000USD and 10000XRP amm with all liquidity + // provided by one LP, LP can not withdraw all tokens in USD. + // Withdrawing 90% in USD is also invalid. Besides the impact + // on the pool there should be a max threshold for single + // deposit. + + // IOU to IOU + transfer fee + { + Env env{*this}; + fund(env, gw, {alice}, {USD(25000), BTC(0.625)}, Fund::All); + env(rate(gw, 1.25)); + AMM ammAlice(env, alice, USD(20000), BTC(0.5)); + BEAST_EXPECT(ammAlice.expectBalances( + USD(20000), BTC(0.5), IOUAmount{100, 0})); + // Transfer fee is not charged. + BEAST_EXPECT(expectLine(env, alice, USD(5000))); + BEAST_EXPECT(expectLine(env, alice, BTC(0.125))); + // LP deposits, doesn't pay transfer fee. + fund(env, gw, {carol}, {USD(2500), BTC(0.0625)}, Fund::Acct); + ammAlice.deposit(carol, 10); + BEAST_EXPECT(ammAlice.expectBalances( + USD(22000), BTC(0.55), IOUAmount{110, 0})); + BEAST_EXPECT(expectLine(env, carol, USD(500))); + BEAST_EXPECT(expectLine(env, carol, BTC(0.0125))); + // LP withdraws, AMM doesn't pay the transfer fee. + ammAlice.withdraw(carol, 10); + BEAST_EXPECT(ammAlice.expectBalances( + USD(20000), BTC(0.5), IOUAmount{100, 0})); + ammAlice.expectLPTokens(carol, IOUAmount{0, 0}); + BEAST_EXPECT(expectLine(env, carol, USD(2500))); + BEAST_EXPECT(expectLine(env, carol, BTC(0.0625))); + } + } + + void + testInvalidFeeVote() + { + testcase("Invalid Fee Vote"); + using namespace jtx; + + // Invalid flags + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.vote( + std::nullopt, + 1000, + tfAMMWithdrawAll, + std::nullopt, + ter(temINVALID_FLAG)); + }); + + // Invalid fee. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.vote( + std::nullopt, + 65001, + std::nullopt, + std::nullopt, + ter(temBAD_FEE)); + BEAST_EXPECT(ammAlice.expectTradingFee(0)); + }); + + // Invalid Account + testAMM([&](AMM& ammAlice, Env& env) { + Account bad("bad"); + env.memoize(bad); + ammAlice.vote(bad, 1000, std::nullopt, seq(1), ter(terNO_ACCOUNT)); + }); + + // Invalid AMM + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdrawAll(alice); + ammAlice.vote( + alice, 1000, std::nullopt, std::nullopt, ter(terNO_ACCOUNT)); + }); + + // Account is not LP + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.vote( + carol, + 1000, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }); + + // Eight votes fill all voting slots. + // New vote, new account. Fails since the account has + // fewer tokens share than in the vote slots. + testAMM([&](AMM& ammAlice, Env& env) { + auto vote = [&](int i, + std::int16_t tokens, + std::optional ter = std::nullopt) { + Account a(std::to_string(i)); + fund(env, gw, {a}, {USD(1000)}, Fund::Acct); + ammAlice.deposit(a, tokens); + ammAlice.vote( + a, 500 * (i + 1), std::nullopt, std::nullopt, ter); + }; + for (int i = 0; i < 8; ++i) + vote(i, 10000); + BEAST_EXPECT(ammAlice.expectTradingFee(2250)); + vote(8, 10000, ter(tecAMM_FAILED_VOTE)); + }); + } + + void + testFeeVote() + { + testcase("Fee Vote"); + using namespace jtx; + + // One vote sets fee to 1%. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.vote({}, 1000); + BEAST_EXPECT(ammAlice.expectTradingFee(1000)); + }); + + // Eight votes fill all voting slots, set fee 2.25%. + testAMM([&](AMM& ammAlice, Env& env) { + for (int i = 0; i < 8; ++i) + { + Account a(std::to_string(i)); + fund(env, gw, {a}, {USD(1000)}, Fund::Acct); + ammAlice.deposit(a, 10000); + ammAlice.vote(a, 500 * (i + 1)); + } + BEAST_EXPECT(ammAlice.expectTradingFee(2250)); + }); + + // Eight votes fill all voting slots, set fee 2.25%. + // New vote, same account, sets fee 2.75% + testAMM([&](AMM& ammAlice, Env& env) { + auto vote = [&](Account const& a, int i) { + fund(env, gw, {a}, {USD(1000)}, Fund::Acct); + ammAlice.deposit(a, 10000); + ammAlice.vote(a, 500 * (i + 1)); + }; + Account a("0"); + vote(a, 0); + for (int i = 1; i < 8; ++i) + { + Account a(std::to_string(i)); + vote(a, i); + } + BEAST_EXPECT(ammAlice.expectTradingFee(2250)); + ammAlice.vote(a, 4500); + BEAST_EXPECT(ammAlice.expectTradingFee(2750)); + }); + + // Eight votes fill all voting slots, set fee 2.25%. + // New vote, new account, higher vote weight, set higher fee 2.945% + testAMM([&](AMM& ammAlice, Env& env) { + auto vote = [&](int i, std::uint32_t tokens) { + Account a(std::to_string(i)); + fund(env, gw, {a}, {USD(1000)}, Fund::Acct); + ammAlice.deposit(a, tokens); + ammAlice.vote(a, 500 * (i + 1)); + }; + for (int i = 0; i < 8; ++i) + vote(i, 10000); + BEAST_EXPECT(ammAlice.expectTradingFee(2250)); + vote(8, 20000); + BEAST_EXPECT(ammAlice.expectTradingFee(2945)); + }); + + // Eight votes fill all voting slots, set fee 2.75%. + // New vote, new account, higher vote weight, set smaller fee 2.056% + testAMM([&](AMM& ammAlice, Env& env) { + auto vote = [&](int i, std::uint32_t tokens) { + Account a(std::to_string(i)); + fund(env, gw, {a}, {USD(1000)}, Fund::Acct); + ammAlice.deposit(a, tokens); + ammAlice.vote(a, 500 * (i + 1)); + }; + for (int i = 8; i > 0; --i) + vote(i, 10000); + BEAST_EXPECT(ammAlice.expectTradingFee(2750)); + vote(0, 20000); + BEAST_EXPECT(ammAlice.expectTradingFee(2056)); + }); + } + + void + testInvalidBid() + { + testcase("Invalid Bid"); + using namespace jtx; + using namespace std::chrono; + + // Invalid flags + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.bid( + carol, + 0, + std::nullopt, + {}, + tfAMMWithdrawAll, + std::nullopt, + ter(temINVALID_FLAG)); + }); + + // Invalid bid options with [Min,Max]SlotPrice + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + ammAlice.bid( + carol, + 100, + 100, + {}, + std::nullopt, + std::nullopt, + ter(temBAD_AMM_OPTIONS)); + }); + + // Invalid Bid price 0 + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + ammAlice.bid( + carol, + 0, + std::nullopt, + {}, + std::nullopt, + std::nullopt, + ter(temBAD_AMM_TOKENS)); + }); + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + ammAlice.bid( + carol, + std::nullopt, + 0, + {}, + std::nullopt, + std::nullopt, + ter(temBAD_AMM_TOKENS)); + }); + + // Invalid Account + testAMM([&](AMM& ammAlice, Env& env) { + Account bad("bad"); + env.memoize(bad); + ammAlice.bid( + bad, + std::nullopt, + 100, + {}, + std::nullopt, + seq(1), + ter(terNO_ACCOUNT)); + }); + + // Invalid AMM + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.withdrawAll(alice); + ammAlice.bid( + alice, + std::nullopt, + 100, + {}, + std::nullopt, + std::nullopt, + ter(terNO_ACCOUNT)); + }); + + // Auth account is invalid. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.bid( + carol, + 100, + std::nullopt, + {bob}, + std::nullopt, + std::nullopt, + ter(terNO_ACCOUNT)); + }); + + // More than four Auth accounts. + testAMM([&](AMM& ammAlice, Env& env) { + Account ed("ed"); + Account bill("bill"); + Account scott("scott"); + Account james("james"); + env.fund(XRP(1000), bob, ed, bill, scott, james); + env.close(); + ammAlice.deposit(carol, 1000000); + ammAlice.bid( + carol, + 100, + std::nullopt, + {bob, ed, bill, scott, james}, + std::nullopt, + std::nullopt, + ter(temBAD_AMM_OPTIONS)); + }); + + // Bid price exceeds LP owned tokens + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + ammAlice.bid( + carol, + 1000001, + std::nullopt, + {}, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + ammAlice.bid( + carol, + std::nullopt, + 1000001, + {}, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }); + } + + void + testBid() + { + testcase("Bid"); + using namespace jtx; + using namespace std::chrono; + + // Bid 100 tokens. The slot is not owned and the MinSlotPrice is 110 + // (currently 0.001% of the pool token balance). + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + ammAlice.bid(carol, 100); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110, 0})); + // 100 tokens are burned. + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11000), USD(11000), IOUAmount{10999890, 0})); + }); + + // Start bid at computed price. The slot is not owned and the + // MinSlotPrice is 110. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + // Bid, pay the computed price. + ammAlice.bid(carol); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110, 0})); + + fund(env, gw, {bob}, {USD(10000)}, Fund::Acct); + ammAlice.deposit(bob, 1000000); + // Bid, pay the computed price. + ammAlice.bid(bob); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1155, -1})); + + // Bid MaxSlotPrice fails because the computed price is higher. + ammAlice.bid( + carol, + std::nullopt, + 120, + {}, + std::nullopt, + std::nullopt, + ter(tecAMM_FAILED_BID)); + // Bid MaxSlotPrice succeeds. + ammAlice.bid(carol, std::nullopt, 135); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{135, 0})); + }); + + // Slot states. + testAMM([&](AMM& ammAlice, Env& env) { + auto constexpr intervalDuration = 24 * 3600 / 20; + ammAlice.deposit(carol, 1000000); + + fund(env, gw, {bob}, {USD(10000)}, Fund::Acct); + ammAlice.deposit(bob, 1000000); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12000), USD(12000), IOUAmount{12000000, 0})); + + // Initial state, not owned. Default MinSlotPrice. + ammAlice.bid(carol); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{120, 0})); + + // 1st Interval after close, price for 0th interval. + ammAlice.bid(bob); + env.close(seconds(intervalDuration + 1)); + BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{126, 0})); + + // 10th Interval after close, price for 1st interval. + ammAlice.bid(carol); + env.close(seconds(10 * intervalDuration + 1)); + BEAST_EXPECT( + ammAlice.expectAuctionSlot(0, 10, IOUAmount{252298737, -6})); + + // 20th Interval (expired) after close, price for 11th interval. + ammAlice.bid(bob); + env.close(seconds(20 * intervalDuration + 1)); + BEAST_EXPECT(ammAlice.expectAuctionSlot( + 0, 0, IOUAmount{384912158551263, -12})); + + // 0 Interval. + ammAlice.bid(carol); + BEAST_EXPECT(ammAlice.expectAuctionSlot( + 0, 0, IOUAmount{119996367684391, -12})); + // ~363.232 tokens burned on bidding fees. + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12000), USD(12000), IOUAmount{1199951677207142, -8})); + }); + + // Pool's fee 1%. Bid to pay computed price. + // Auction slot owner and auth account trade at discounted fee (0). + // Other accounts trade at 1% fee. + testAMM( + [&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(10000)}, Fund::Acct); + ammAlice.deposit(bob, 1000000); + ammAlice.deposit(carol, 1000000); + ammAlice.bid(carol, std::nullopt, std::nullopt, {bob}); + BEAST_EXPECT( + ammAlice.expectAuctionSlot(0, 0, IOUAmount{120, 0})); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12000), USD(12000), IOUAmount{11999880, 0})); + // Discounted trade + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(carol, USD(100)); + ammAlice.withdraw(carol, USD(100)); + ammAlice.deposit(bob, USD(100)); + ammAlice.withdraw(bob, USD(100)); + } + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12000), USD(12000), IOUAmount{119998799999998, -7})); + // Trade with the fee + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(alice, USD(100)); + ammAlice.withdraw(alice, USD(100)); + } + BEAST_EXPECT(ammAlice.expectBalances( + XRP(12000), USD(12000), IOUAmount{1199488908260979, -8})); + }, + std::nullopt, + std::nullopt, + 1000); + } + + void + testInvalidAMMPayment() + { + testcase("Invalid AMM Payment"); + using namespace jtx; + + // Can't pay into AMM account. + // Can't pay out since there is no keys + testAMM([&](AMM& ammAlice, Env& env) { + env(pay(carol, ammAlice.ammAccount(), XRP(10)), + ter(tecAMM_DIRECT_PAYMENT)); + env(pay(carol, ammAlice.ammAccount(), USD(10)), + ter(tecAMM_DIRECT_PAYMENT)); + }); + } + + void + testBasicPaymentEngine() + { + testcase("Basic Payment"); + using namespace jtx; + + // Partial payment ~99.0099USD for 100XRP. + // Force one path with tfNoRippleDirect. + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(jtx::XRP(30000), bob); + env.close(); + env(pay(bob, carol, USD(100)), + path(~USD), + sendmax(XRP(100)), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10100), + STAmount(USD, UINT64_C(9900990099009901), -12), + IOUAmount{10000000, 0})); + // Initial balance 30,000 + 99.0099009901 + BEAST_EXPECT(expectLine( + env, carol, STAmount{USD, UINT64_C(300990099009901), -10})); + // Initial balance 30,000 - 100(sendmax) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(30000) - XRP(100) - txfee(env, 1))); + }); + + // Partial payment ~99.0099USD for 100XRP, use default path. + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(jtx::XRP(30000), bob); + env.close(); + env(pay(bob, carol, USD(100)), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10100), + STAmount(USD, UINT64_C(9900990099009901), -12), + IOUAmount{10000000, 0})); + // Initial balance 30,000 + 99.0099009901 + BEAST_EXPECT(expectLine( + env, carol, STAmount{USD, UINT64_C(300990099009901), -10})); + // Initial balance 30,000 - 100(sendmax) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(30000) - XRP(100) - txfee(env, 1))); + }); + + // This payment is identical to above. While it has + // both default path and path, activeStrands has one path. + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(jtx::XRP(30000), bob); + env.close(); + env(pay(bob, carol, USD(100)), + path(~USD), + sendmax(XRP(100)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10100), + STAmount(USD, UINT64_C(9900990099009901), -12), + IOUAmount{10000000, 0})); + // Initial balance 30,000 + 99.0099009901 + BEAST_EXPECT(expectLine( + env, carol, STAmount{USD, UINT64_C(300990099009901), -10})); + // Initial balance 30,000 - 100(sendmax) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(30000) - XRP(100) - txfee(env, 1))); + }); + + // Non-default path (with AMM) has a better quality than default path. + // The max possible liquidity is taken out of non-default + // path ~17.5XRP/17.5EUR, 17.5EUR/~17.47USD. The rest + // is taken from the offer. + { + Env env(*this); + fund(env, gw, {alice, carol}, {USD(30000), EUR(30000)}, Fund::All); + env.close(); + env.fund(XRP(1000), bob); + env.close(); + auto ammEUR_XRP = AMM(env, alice, XRP(10000), EUR(10000)); + auto ammUSD_EUR = AMM(env, alice, EUR(10000), USD(10000)); + env(offer(alice, XRP(101), USD(100)), txflags(tfPassive)); + env.close(); + env(pay(bob, carol, USD(100)), + path(~EUR, ~USD), + sendmax(XRP(102)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammEUR_XRP.expectBalances( + XRPAmount(10017526291), + STAmount(EUR, UINT64_C(9982504373906523), -12), + IOUAmount{10000000, 0})); + BEAST_EXPECT(ammUSD_EUR.expectBalances( + STAmount(USD, UINT64_C(9982534949910309), -12), + STAmount(EUR, UINT64_C(1001749562609347), -11), + IOUAmount{10000, 0})); + BEAST_EXPECT(expectOffers( + env, + alice, + 1, + {{Amounts{ + XRPAmount(17639700), + STAmount(USD, UINT64_C(1746505008969044), -14)}}})); + // Initial 30,000 + 100 + BEAST_EXPECT(expectLine(env, carol, STAmount{USD, 30100})); + // Initial 1,000 - 17526291(AMM pool) - 83360300(offer) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + bob, + XRP(1000) - XRPAmount{17526291} - XRPAmount{83360300} - + txfee(env, 1))); + } + + // Default path (with AMM) has a better quality than a non-default path. + // The max possible liquidity is taken out of default + // path ~17.5XRP/17.5USD. The rest is taken from the offer. + testAMM([&](AMM& ammAlice, Env& env) { + env.fund(XRP(1000), bob); + env.close(); + env.trust(EUR(2000), alice); + env.close(); + env(pay(gw, alice, EUR(1000))); + env(offer(alice, XRP(101), EUR(100)), txflags(tfPassive)); + env.close(); + env(offer(alice, EUR(100), USD(100)), txflags(tfPassive)); + env.close(); + env(pay(bob, carol, USD(100)), + path(~EUR, ~USD), + sendmax(XRP(102)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10017526291), + STAmount(USD, UINT64_C(9982504373906523), -12), + IOUAmount{10000000, 0})); + BEAST_EXPECT(expectOffers( + env, + alice, + 2, + {{Amounts{ + XRPAmount(17670582), + STAmount(EUR, UINT64_C(17495626093477), -12)}, + Amounts{ + STAmount(EUR, UINT64_C(17495626093477), -12), + STAmount(USD, UINT64_C(17495626093477), -12)}}})); + // Initial 30,000 + 99.99999999999 + BEAST_EXPECT(expectLine( + env, carol, STAmount{USD, UINT64_C(3009999999999999), -11})); + // Initial 1,000 - 10017526291(AMM pool) - 83329418(offer) - 10(tx + // fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + bob, + XRP(1000) - XRPAmount{17526291} - XRPAmount{83329418} - + txfee(env, 1))); + }); + + // Default path with AMM and Order Book offer. AMM is consumed first, + // remaining amount is consumed by the offer. + testAMM( + [&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(100)}, Fund::Acct); + env.close(); + env(offer(bob, XRP(100), USD(100)), txflags(tfPassive)); + env.close(); + env(pay(alice, carol, USD(200)), + sendmax(XRP(200)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(9999499987), + STAmount(USD, UINT64_C(9999499987998749), -12), + IOUAmount{999949998749938, -8})); + // Initial 30,000 + 200 + BEAST_EXPECT(expectLine(env, carol, STAmount{USD, 30200})); + // Initial 30,000 - 9,900(AMM pool LP) - 99499987(AMM offer) - + // - 99499988(offer) - 20(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, + alice, + XRP(30000) - XRP(9900) - XRPAmount{99499987} - + XRPAmount{99499988} - txfee(env, 2))); + BEAST_EXPECT(expectOffers( + env, + bob, + 1, + {{{XRPAmount{500012}, + STAmount{USD, UINT64_C(5000120012508), -13}}}})); + }, + std::make_pair(XRP(9900), USD(10100)), + IOUAmount{999949998749938, -8}); + + // Offer crossing + testAMM( + [&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(1000)}, Fund::Acct); + env.close(); + env(offer(bob, USD(100), XRP(100))); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(9999000000), + STAmount(USD, 10000), + IOUAmount{999949998749938, -8})); + // Initial 1,000 + 100 + BEAST_EXPECT(expectLine(env, bob, STAmount{USD, 1100})); + // Initial 30,000 - 99(offer) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(30000) - XRP(99) - txfee(env, 1))); + env.require(offers(bob, 0)); + }, + std::make_pair(XRP(9900), USD(10100)), + IOUAmount{999949998749938, -8}); + + // Partial offer crossing. Smaller offer is consumed because of + // the quality limit. + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, {USD(1000)}, Fund::Acct); + env.close(); + env(offer(bob, USD(99), XRP(100))); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{10050378153}, + STAmount(USD, UINT64_C(99498743710662), -10), + IOUAmount{10000000, 0})); + // Initial 1,000 + 50.1256289338(offer) + BEAST_EXPECT(expectLine( + env, bob, STAmount{USD, UINT64_C(10501256289338), -10})); + // Initial 30,000 - 50378153(AMM offer) - 10(tx fee) + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(30000) - XRPAmount{50378153} - txfee(env, 1))); + BEAST_EXPECT(expectOffers( + env, + bob, + 1, + {{Amounts{ + STAmount{USD, UINT64_C(488743710662), -10}, + XRPAmount{49368052}}}})); + }); + } + + void + testAMMTokens() + { + testcase("AMM Token Pool - AMM with token from another AMM"); + using namespace jtx; + + // AMM with one LPToken from another AMM. + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {alice}, {EUR(10000)}, Fund::None); + AMM ammAMMToken( + env, alice, EUR(10000), STAmount{ammAlice.lptIssue(), 1000000}); + BEAST_EXPECT(ammAMMToken.expectBalances( + EUR(10000), + STAmount(ammAlice.lptIssue(), 1000000), + IOUAmount{100000, 0})); + }); + + // AMM with two LPTokens from other AMMs. + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {alice}, {EUR(10000)}, Fund::None); + AMM ammAlice1(env, alice, XRP(10000), EUR(10000)); + auto const token1 = ammAlice.lptIssue(); + auto const token2 = ammAlice1.lptIssue(); + AMM ammAMMTokens( + env, + alice, + STAmount{token1, 1000000}, + STAmount{token2, 1000000}); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, 1000000), + STAmount(token2, 1000000), + IOUAmount{1000000, 0})); + }); + + // AMM with two LPTokens from other AMMs. + // LP deposits/withdraws. + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {alice}, {EUR(10000)}, Fund::None); + AMM ammAlice1(env, alice, XRP(10000), EUR(10000)); + auto const token1 = ammAlice.lptIssue(); + auto const token2 = ammAlice1.lptIssue(); + AMM ammAMMTokens( + env, + alice, + STAmount{token1, 1000000}, + STAmount{token2, 1000000}); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, 1000000), + STAmount(token2, 1000000), + IOUAmount{1000000, 0})); + ammAMMTokens.deposit(alice, 10000); + ammAMMTokens.withdraw(alice, 10000); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, 1000000), + STAmount(token2, 1000000), + IOUAmount{1000000, 0})); + }); + + // Offer crossing with two AMM LPtokens. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + fund(env, gw, {alice, carol}, {EUR(10000)}, Fund::None); + AMM ammAlice1(env, alice, XRP(10000), EUR(10000)); + ammAlice1.deposit(carol, 1000000); + auto const token1 = ammAlice.lptIssue(); + auto const token2 = ammAlice1.lptIssue(); + env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}), + txflags(tfPassive)); + env.close(); + env.require(offers(alice, 1)); + env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100})); + env.close(); + BEAST_EXPECT( + expectLine(env, alice, STAmount{token1, 10000100}) && + expectLine(env, alice, STAmount{token2, 9999900})); + BEAST_EXPECT( + expectLine(env, carol, STAmount{token2, 1000100}) && + expectLine(env, carol, STAmount{token1, 999900})); + env.require(offers(alice, 0), offers(carol, 0)); + }); + + // Offer crossing with two AMM LPTokens via AMM. + testAMM([&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1000000); + fund(env, gw, {alice, carol}, {EUR(10000)}, Fund::None); + AMM ammAlice1(env, alice, XRP(10000), EUR(10000)); + ammAlice1.deposit(carol, 1000000); + auto const token1 = ammAlice.lptIssue(); + auto const token2 = ammAlice1.lptIssue(); + AMM ammAMMTokens( + env, alice, STAmount{token1, 9900}, STAmount{token2, 10100}); + env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100})); + env.close(); + env.require(offers(carol, 0)); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, 9999), + STAmount(token2, 10000), + IOUAmount{999949998749938, -11})); + // Carol initial token1 1,000,000 - 99(offer) + BEAST_EXPECT(expectLine(env, carol, STAmount{token1, 999901})); + // Carol initial token2 1,000,000 + 100(offer) + BEAST_EXPECT(expectLine(env, carol, STAmount{token2, 1000100})); + }); + + // LPs pay LPTokens directly. Must trust set . + testAMM([&](AMM& ammAlice, Env& env) { + auto const token1 = ammAlice.lptIssue(); + env.trust(STAmount{token1, 2000000}, carol); + env.close(); + ammAlice.deposit(carol, 1000000); + BEAST_EXPECT( + ammAlice.expectLPTokens(alice, IOUAmount{10000000, 0}) && + ammAlice.expectLPTokens(carol, IOUAmount{1000000, 0})); + // Pool balance doesn't change, only tokens moved from + // one line to another. + env(pay(alice, carol, STAmount{token1, 100})); + env.close(); + BEAST_EXPECT( + // Alice initial token1 10,000,000 - 100 + ammAlice.expectLPTokens(alice, IOUAmount{9999900, 0}) && + // Carol initial token1 1,000,000 + 100 + ammAlice.expectLPTokens(carol, IOUAmount{1000100, 0})); + }); + + // AMM with two tokens from another AMM. + // LP pays LPTokens to non-LP via AMM. + // Non-LP must trust set for LPTokens. + testAMM([&](AMM& ammAlice, Env& env) { + fund(env, gw, {alice}, {EUR(10000)}, Fund::None); + AMM ammAlice1(env, alice, XRP(10000), EUR(10000)); + auto const token1 = ammAlice.lptIssue(); + auto const token2 = ammAlice1.lptIssue(); + AMM ammAMMTokens( + env, + alice, + STAmount{token1, 1000000}, + STAmount{token2, 1000000}); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, 1000000), + STAmount(token2, 1000000), + IOUAmount{1000000, 0})); + env.trust(STAmount{token1, 1000}, carol); + env.close(); + env(pay(alice, carol, STAmount{token1, 100}), + path(BookSpec(token1.account, token1.currency)), + sendmax(STAmount{token2, 100}), + txflags(tfPartialPayment | tfNoRippleDirect)); + env.close(); + BEAST_EXPECT(ammAMMTokens.expectBalances( + STAmount(token1, UINT64_C(9999000099990001), -10), + STAmount(token2, 1000100), + IOUAmount{1000000, 0})); + // Alice's token1 balance doesn't change after the payment. + // The payment comes out of AMM pool. Alice's token1 balance + // is initial 10,000,000 - 1,000,000 deposited into ammAMMTokens + // pool. + BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{9000000})); + // Carol got ~99.99 token1 from ammAMMTokens pool. Alice swaps + // in 100 token2 into ammAMMTokens pool. + BEAST_EXPECT( + ammAlice.expectLPTokens(carol, IOUAmount{999900009999, -10})); + // Alice's token2 balance changes. Initial 10,000,000 - 1,000,000 + // deposited into ammAMMTokens pool - 100 payment. + BEAST_EXPECT(ammAlice1.expectLPTokens(alice, IOUAmount{8999900})); + }); + } + + void + testEnforceNoRipple(FeatureBitset features) + { + testcase("Enforce No Ripple"); + using namespace jtx; + + { + // No ripple with an implied account step after an offer + Env env{*this, features}; + + Account const dan("dan"); + Account const gw1("gw1"); + Account const gw2("gw2"); + auto const USD1 = gw1["USD"]; + auto const USD2 = gw2["USD"]; + + env.fund(XRP(20000), alice, noripple(bob), carol, dan, gw1, gw2); + env.trust(USD1(20000), alice, carol, dan); + env(trust(bob, USD1(1000), tfSetNoRipple)); + env.trust(USD2(1000), alice, carol, dan); + env(trust(bob, USD2(1000), tfSetNoRipple)); + + env(pay(gw1, dan, USD1(10000))); + env(pay(gw1, bob, USD1(50))); + env(pay(gw2, bob, USD2(50))); + + AMM ammDan(env, dan, XRP(10000), USD1(10000)); + + env(pay(alice, carol, USD2(50)), + path(~USD1, bob), + sendmax(XRP(50)), + txflags(tfNoRippleDirect), + ter(tecPATH_DRY)); + } + + { + // Make sure payment works with default flags + Env env{*this, features}; + + Account const dan("dan"); + Account const gw1("gw1"); + Account const gw2("gw2"); + auto const USD1 = gw1["USD"]; + auto const USD2 = gw2["USD"]; + + env.fund(XRP(20000), alice, bob, carol, dan, gw1, gw2); + env.trust(USD1(20000), alice, bob, carol, dan); + env.trust(USD2(1000), alice, bob, carol, dan); + + env(pay(gw1, dan, USD1(10000))); + env(pay(gw1, bob, USD1(50))); + env(pay(gw2, bob, USD2(50))); + + AMM ammDan(env, dan, XRP(10000), USD1(10000)); + + env(pay(alice, carol, USD2(50)), + path(~USD1, bob), + sendmax(XRP(60)), + txflags(tfNoRippleDirect)); + BEAST_EXPECT(ammDan.expectBalances( + XRPAmount{10050251257}, USD1(9950), IOUAmount{10000000, 0})); + + env.require(balance( + alice, + 20000 * dropsPerXRP - XRPAmount{50251257} - txfee(env, 1))); + env.require(balance(bob, USD1(100))); + env.require(balance(bob, USD2(0))); + env.require(balance(carol, USD2(50))); + } + } + + void + testFillModes(FeatureBitset features) + { + testcase("Fill Modes"); + using namespace jtx; + + auto const startBalance = XRP(1000000); + + // Fill or Kill - unless we fully cross, just charge a fee and don't + // place the offer on the books. But also clean up expired offers + // that are discovered along the way. + // + // fix1578 changes the return code. Verify expected behavior + // without and with fix1578. + for (auto const& tweakedFeatures : + {features - fix1578, features | fix1578}) + { + // Order that can't be filled + { + Env env{*this, tweakedFeatures}; + + auto const f = txfee(env, 1); + + fund(env, gw, {alice, bob}, startBalance); + env.close(); + env(trust(alice, USD(20000)), ter(tesSUCCESS)); + env.close(); + env(trust(bob, USD(500)), ter(tesSUCCESS)); + env.close(); + env(pay(gw, alice, USD(15000)), ter(tesSUCCESS)); + env.close(); + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + env.close(); + TER const killedCode{ + tweakedFeatures[fix1578] ? TER{tecKILLED} + : TER{tesSUCCESS}}; + env(offer(bob, USD(500), XRP(500)), + txflags(tfFillOrKill), + ter(killedCode)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + env.require( + balance( + alice, + startBalance - XRP(10000) - (f * 2)), // trust+AMM + owners(bob, 1), + offers(bob, 0)); + } + + { + Env env{*this, tweakedFeatures}; + + fund(env, gw, {alice, bob}, startBalance); + env.close(); + env(trust(alice, USD(20000)), ter(tesSUCCESS)); + env.close(); + env(trust(bob, USD(1000)), ter(tesSUCCESS)); + env.close(); + env(pay(gw, alice, USD(15000)), ter(tesSUCCESS)); + env.close(); + env(pay(gw, bob, USD(500)), ter(tesSUCCESS)); + env.close(); + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + // TODO + // Order that can be filled has it been another offer + // instead of AMM. Does this work in practice with AMM? + // There is no exact match. +#if 0 + env(offer(bob, XRP(500), USD(500)), + txflags(tfFillOrKill), + ter(tesSUCCESS)); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + env.require( + balance( + alice, + startBalance - XRP(10000) - (f * 2)), // trust + AMM + owners(bob, 1), + offers(bob, 0)); +#endif + } + + // Immediate or Cancel - cross as much as possible + // and add nothing on the books: + // Partially cross + { + Env env{*this, features}; + + auto const f = txfee(env, 1); + + fund(env, gw, {alice, bob}, startBalance); + + env(trust(alice, USD(1000)), ter(tesSUCCESS)); + env(pay(gw, alice, USD(1000)), ter(tesSUCCESS)); + env(trust(bob, USD(20000)), ter(tesSUCCESS)); + env(pay(gw, bob, USD(15000)), ter(tesSUCCESS)); + + AMM ammBob(env, bob, XRP(11000), USD(10000)); + env(offer(alice, XRP(1000), USD(1000)), + txflags(tfImmediateOrCancel), + ter(tesSUCCESS)); + BEAST_EXPECT(ammBob.expectBalances( + XRPAmount{10488088482}, + STAmount{USD, UINT64_C(1048808848140303), -11}, + IOUAmount{1048808848170152, -8})); + + env.require( + // + AMM - (trust + offer) * fee + balance(alice, startBalance - f - f + XRPAmount{511911518}), + // AMM + balance( + alice, STAmount{USD, UINT64_C(51191151859697), -11}), + owners(alice, 1), + offers(alice, 0), + // -AMM - (trust + AMM) * fee + balance(bob, startBalance - XRP(11000) - f - f), + balance(bob, USD(5000)), + // USD + LPTokens + owners(bob, 2)); + } + + // Fully cross: + { + Env env{*this, features}; + + auto const f = txfee(env, 1); + + fund(env, gw, {alice, bob}, startBalance); + + env(trust(alice, USD(1000)), ter(tesSUCCESS)); + env(pay(gw, alice, USD(1000)), ter(tesSUCCESS)); + env(trust(bob, USD(20000)), ter(tesSUCCESS)); + env(pay(gw, bob, USD(15000)), ter(tesSUCCESS)); + + // Consumed 1000XRP/900USD + AMM ammBob(env, bob, XRP(11000), USD(9000)); + env(offer(alice, XRP(1000), USD(1000)), + txflags(tfImmediateOrCancel), + ter(tesSUCCESS)); + BEAST_EXPECT(ammBob.expectBalances( + XRP(10000), USD(9900), IOUAmount{99498743710662, -7})); + + env.require( + // + AMM - (trust + offer) * fee + balance(alice, startBalance - f - f + XRP(1000)), + // AMM + balance(alice, USD(100)), + owners(alice, 1), + offers(alice, 0), + // -AMM - (trust + AMM) * fee + balance(bob, startBalance - XRP(11000) - f - f), + balance(bob, USD(6000)), + // USD + LPTokens + owners(bob, 2)); + } + + // tfPassive -- place the offer without crossing it. + { + Env env(*this, features); + + fund(env, gw, {alice, bob}, startBalance); + + env(trust(bob, USD(1000))); + env.close(); + + env(pay(gw, bob, USD(1000))); + env.close(); + + env(trust(alice, USD(20000))); + env.close(); + + env(pay(gw, alice, USD(15000))); + env.close(); + AMM ammAlice(env, alice, XRP(11000), USD(9000)); + env.close(); + + // TODO + // Does this work in practice with AMM? + // There is no exact match. + // bob creates a passive offer that could cross AMM. + // bob's offer should stay in the ledger. +#if 0 + env(offer(bob, XRP(1000), USD(1000), tfPassive)); + env.close(); + BEAST_EXPECT(expectOffers( + env, bob, 1, {{{XRPAmount{1000}, STAmount{USD, 1000}}}})); +#endif + } + + // tfPassive -- cross only offers of better quality. + { + Env env(*this, features); + + fund(env, gw, {alice, bob}, startBalance); + + env(trust(bob, USD(1000))); + env.close(); + + env(pay(gw, bob, USD(1000))); + env.close(); + + env(trust(alice, USD(20000))); + env.close(); + + env(pay(gw, alice, USD(15000))); + env.close(); + AMM ammAlice(env, alice, XRP(11000), USD(9000)); + env.close(); + env(offer(alice, USD(1101), XRP(900))); + env.close(); + + // bob creates a passive offer. That offer should cross AMM + // and leave alice's offer untouched. + env(offer(bob, XRP(1000), USD(1000), tfPassive)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10000), USD(9900), IOUAmount{99498743710662, -7})); + BEAST_EXPECT(expectOffers(env, bob, 0)); + BEAST_EXPECT(expectOffers(env, alice, 1)); + } + } + } + + void + testOfferCrossWithXRP(FeatureBitset features) + { + testcase("Offer Crossing with XRP, Normal order"); + + using namespace jtx; + + Env env{*this, features}; + + fund(env, gw, {bob}, XRP(10000)); + env.fund(XRP(210000), alice); + + env(trust(alice, USD(1000))); + env(trust(bob, USD(1000))); + + env(pay(gw, alice, alice["USD"](500))); + + AMM ammAlice(env, alice, XRP(150000), USD(50)); + + env(offer(bob, USD(1), XRP(4000))); + + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{153061224490}, USD(49), IOUAmount{273861278752583, -8})); + + // Existing offer pays better than this wants. + // Partially consume existing offer. + // Pay 1 USD, get 3061224489 Drops. + auto const xrpConsumed = XRPAmount{3061224490}; + + BEAST_EXPECT(expectLine(env, bob, STAmount{USD, 1})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, bob, XRP(10000) - xrpConsumed - txfee(env, 2))); + + BEAST_EXPECT(expectLine(env, alice, STAmount{USD, 450})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, XRP(210000) - XRP(150000) - txfee(env, 2))); + } + + void + testCurrencyConversionPartial(FeatureBitset features) + { + testcase("Currency Conversion: In Parts"); + + using namespace jtx; + + Env env{*this, features}; + + fund(env, gw, {alice, bob}, {USD(20000)}, Fund::All); + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + + // Alice converts USD to XRP which should fail + // due to PartialPayment. + env(pay(alice, alice, XRP(600)), + sendmax(USD(100)), + ter(tecPATH_PARTIAL)); + + // Alice converts USD to XRP, should succeed because + // we permit partial payment + env(pay(alice, alice, XRP(600)), + sendmax(USD(100)), + txflags(tfPartialPayment)); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9900990100}, USD(10100), IOUAmount{10000000})); + } + + void + testCrossCurrencyStartXRP(FeatureBitset features) + { + testcase("Cross Currency Payment: Start with XRP"); + + using namespace jtx; + + Env env{*this, features}; + + fund(env, gw, {alice, bob}, {USD(10000)}, Fund::All); + + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + + env(pay(alice, bob, USD(100)), sendmax(XRP(120))); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{10101010102}, USD(9900), IOUAmount{10000000})); + BEAST_EXPECT(expectLine(env, bob, STAmount{USD, 10100})); + } + + void + testCrossCurrencyEndXRP(FeatureBitset features) + { + testcase("Cross Currency Payment: End with XRP"); + + using namespace jtx; + + Env env{*this, features}; + + fund(env, gw, {alice, bob}, {USD(10200)}, Fund::All); + + AMM ammAlice(env, alice, XRP(10000), USD(10000)); + + env(pay(alice, bob, XRP(100)), sendmax(USD(120))); + env.close(); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(9900), + STAmount{USD, UINT64_C(101010101010101), -10}, + IOUAmount{10000000})); + } + + void + testCrossCurrencyBridged(FeatureBitset features) + { + testcase("Cross Currency Payment: Bridged"); + + using namespace jtx; + + Env env{*this, features}; + + auto const gw1 = Account{"gateway_1"}; + auto const gw2 = Account{"gateway_2"}; + auto const dan = Account{"dan"}; + auto const USD1 = gw1["USD"]; + auto const EUR1 = gw2["EUR"]; + + fund(env, gw1, {gw2, alice, bob, carol, dan}, XRP(60000)); + + env(trust(alice, USD1(1000))); + env.close(); + env(trust(bob, EUR1(1000))); + env.close(); + env(trust(carol, USD1(10000))); + env.close(); + env(trust(dan, EUR1(1000))); + env.close(); + + env(pay(gw1, alice, alice["USD"](500))); + env.close(); + env(pay(gw1, carol, carol["USD"](6000))); + env(pay(gw2, dan, dan["EUR"](400))); + env.close(); + + AMM ammCarol(env, carol, USD1(5000), XRP(50000)); + + env(offer(dan, XRP(500), EUR1(50))); + env.close(); + + Json::Value jtp{Json::arrayValue}; + jtp[0u][0u][jss::currency] = "XRP"; + env(pay(alice, bob, EUR1(30)), + json(jss::Paths, jtp), + sendmax(USD1(333))); + env.close(); + BEAST_EXPECT(ammCarol.expectBalances( + XRP(49700), + STAmount{USD1, UINT64_C(5030181086519115), -12}, + IOUAmount{158113883008419, -7})); + BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), EUR(20)}}})); + BEAST_EXPECT(expectLine(env, bob, STAmount{EUR1, 30})); + } + + void + testSellFlagBasic(FeatureBitset features) + { + testcase("Offer tfSell: Basic Sell"); + + using namespace jtx; + + testAMM( + [&](AMM& ammAlice, Env& env) { + fund(env, gw, {bob}, XRP(1000), {}, Fund::Acct); + env(offer(bob, USD(100), XRP(100)), json(jss::Flags, tfSell)); + env.close(); + // There is a slight results difference because + // of tfSell flag between this test and offer + // crossing in testBasicPaymentEngine() test. + // The difference is due to how limitQuality + // is handled in one-path AMM optimization. + // In the former test limitQuality doesn't + // change remainingOut. In this test limitQuality + // changes remainingOut, which is 1/2 max because + // of tfSell, to ~100.5USD. This results in + // slightly larger consumed offer 100.5USD/99.5XRP + // as opposed to former of 100USD/99XRP. + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(9999499988), + STAmount{USD, UINT64_C(999949998749938), -11}, + IOUAmount{999949998749938, -8})); + BEAST_EXPECT(expectOffers( + env, + bob, + 1, + {{{STAmount{USD, 500012, -6}, XRPAmount{500012}}}})); + BEAST_EXPECT(expectLine( + env, bob, STAmount{USD, UINT64_C(1005000125006196), -13})); + }, + {{XRP(9900), USD(10100)}}, + IOUAmount{999949998749938, -8}, + 0, + std::nullopt, + features); + } + + void + testAmendment() + { + testcase("Amendment"); + } + + void + testTradingFees() + { + testcase("Trading Fees"); + } + + void + testOffers() + { + using namespace jtx; + FeatureBitset const all{supported_amendments()}; + testEnforceNoRipple(all); + testFillModes(all); + // testUnfundedCross + // testNegativeBalance + testOfferCrossWithXRP(all); + // testOfferCrossWithLimitOverride + // testCurrencyConversionIntoDebt + testCurrencyConversionPartial(all); + testCrossCurrencyStartXRP(all); + testCrossCurrencyEndXRP(all); + testCrossCurrencyBridged(all); + // testBridgedSecondLegDry + testSellFlagBasic(all); + } + + void + testAll() + { + testInvalidInstance(); + testInstanceCreate(); + testInvalidDeposit(); + testDeposit(); + testInvalidWithdraw(); + testWithdraw(); + testInvalidFeeVote(); + testFeeVote(); + testInvalidBid(); + testBid(); + testInvalidAMMPayment(); + testBasicPaymentEngine(); + testAMMTokens(); + } + + void + run() override + { + testAll(); + testOffers(); + } +}; + +struct AMM_manual_test : public Test +{ + void + testFibonacciPerf() + { + testcase("Performance Fibonacci"); + using namespace std::chrono; + auto const start = high_resolution_clock::now(); + + auto const fee = Number(1) / 100; + auto const c1_fee = 1 - fee; + Number poolPays = 1000000; + Number poolGets = 1000000; + auto SP = poolPays / (poolGets * c1_fee); + auto ftakerPays = (Number(5) / 10000) * poolGets / 2; + auto ftakerGets = SP * ftakerPays; + poolGets += ftakerPays; + poolPays -= ftakerGets; + auto product = poolPays * poolGets; + Number x(0); + Number y = ftakerGets; + Number ftotal(0); + for (int i = 0; i < 100; ++i) + { + ftotal = x + y; + ftakerGets = ftotal; + auto ftakerPaysPrime = product / (poolPays - ftakerGets) - poolGets; + ftakerPays = ftakerPaysPrime / c1_fee; + poolGets += ftakerPays; + poolPays -= ftakerGets; + x = y; + y = ftotal; + product = poolPays * poolGets; + } + auto const elapsed = high_resolution_clock::now() - start; + + std::cout << "100 fibonnaci " + << duration_cast(elapsed).count() + << std::endl; + BEAST_EXPECT(true); + } + + void + testOffersPerf() + { + testcase("Performance Offers"); + + auto const N = 10; + std::array t; + + for (auto i = 0; i < N; i++) + { + using namespace jtx; + Env env(*this); + + env.fund(XRP(1000), alice, carol, bob, gw); + env.trust(USD(1000), carol); + env.trust(EUR(1000), alice); + env.trust(USD(1000), bob); + + env(pay(gw, alice, EUR(1000))); + env(pay(gw, bob, USD(1000))); + + env(offer(bob, EUR(1000), USD(1000))); + + auto start = std::chrono::high_resolution_clock::now(); + env(pay(alice, carol, USD(1000)), path(~USD), sendmax(EUR(1000))); + auto elapsed = std::chrono::high_resolution_clock::now() - start; + std::uint64_t microseconds = + std::chrono::duration_cast(elapsed) + .count(); + t[i] = microseconds; + } + stats(t, "single offer"); + + for (auto i = 0; i < N; i++) + { + using namespace jtx; + Env env(*this); + + env.fund(XRP(1000), alice, carol, bob, gw); + env.trust(USD(1000), carol); + env.trust(EUR(1100), alice); + env.trust(USD(1000), bob); + + env(pay(gw, alice, EUR(1100))); + env(pay(gw, bob, USD(1000))); + + for (auto j = 0; j < 10; j++) + env(offer(bob, EUR(100 + j), USD(100))); + + auto start = std::chrono::high_resolution_clock::now(); + env(pay(alice, carol, USD(1000)), path(~USD), sendmax(EUR(1100))); + auto elapsed = std::chrono::high_resolution_clock::now() - start; + std::uint64_t microseconds = + std::chrono::duration_cast(elapsed) + .count(); + t[i] = microseconds; + } + stats(t, "multiple offers"); + } + + void + run() override + { + testFibonacciPerf(); + } +}; + +BEAST_DEFINE_TESTSUITE(AMM, app, ripple); +BEAST_DEFINE_TESTSUITE_MANUAL(AMM_manual, tx, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 4a67ff13f2e..ae8f9163ebe 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -26,6 +26,7 @@ #include #include #include +#include "ripple/app/paths/AMMOfferCounter.h" #include #include @@ -657,6 +658,8 @@ struct PayStrand_test : public beast::unit_test::suite using B = ripple::Book; using XRPS = XRPEndpointStepInfo; + AMMOfferCounter ammOfferCounter(false); + auto test = [&, this]( jtx::Env& env, Issue const& deliver, @@ -674,6 +677,7 @@ struct PayStrand_test : public beast::unit_test::suite path, true, false, + ammOfferCounter, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == expTer); if (sizeof...(expSteps) != 0) @@ -701,6 +705,7 @@ struct PayStrand_test : public beast::unit_test::suite path, true, false, + ammOfferCounter, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -717,6 +722,7 @@ struct PayStrand_test : public beast::unit_test::suite path, true, false, + ammOfferCounter, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -836,6 +842,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), true, false, + ammOfferCounter, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -851,6 +858,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), true, false, + ammOfferCounter, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -866,6 +874,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), true, false, + ammOfferCounter, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -1002,6 +1011,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), true, false, + ammOfferCounter, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal(strand, D{alice, gw, usdC})); @@ -1028,6 +1038,7 @@ struct PayStrand_test : public beast::unit_test::suite path, false, false, + ammOfferCounter, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal( diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index d345b8cc1ed..0ed89fa4fc9 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -28,6 +28,7 @@ #include #include #include +#include "ripple/app/paths/AMMOfferCounter.h" #include #include @@ -245,6 +246,7 @@ class TheoreticalQuality_test : public beast::unit_test::suite std::optional const& expectedQ = {}) { PaymentSandbox sb(closed.get(), tapNONE); + AMMOfferCounter ammOfferCounter(false); auto const sendMaxIssue = [&rcp]() -> std::optional { if (rcp.sendMax) @@ -265,6 +267,7 @@ class TheoreticalQuality_test : public beast::unit_test::suite /*defaultPaths*/ rcp.paths.empty(), sb.rules().enabled(featureOwnerPaysFee), /*offerCrossing*/ false, + ammOfferCounter, dummyJ); BEAST_EXPECT(sr.first == tesSUCCESS); diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h new file mode 100644 index 00000000000..0e2195fe94d --- /dev/null +++ b/src/test/jtx/AMM.h @@ -0,0 +1,260 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_AMM_H_INCLUDED +#define RIPPLE_TEST_JTX_AMM_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Convenience class to test AMM functionality. + */ +class AMM +{ + Env& env_; + Account const creatorAccount_; + uint256 ammID_; + AccountID ammAccount_; + Issue lptIssue_; + STAmount asset1_; + STAmount asset2_; + std::optional ter_; + bool log_ = false; + + void + create( + std::uint32_t tfee = 0, + std::optional flags = std::nullopt, + std::optional seq = std::nullopt); + + void + deposit( + std::optional const& account, + Json::Value& jv, + std::optional const& seq = std::nullopt); + + void + withdraw( + std::optional const& account, + Json::Value& jv, + std::optional const& seq, + std::optional const& ter = std::nullopt); + + void + log(bool log) + { + log_ = log; + } + + bool + expectAmmInfo( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& balance, + Json::Value const& jv) const; + +public: + AMM(Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + bool log = false, + std::uint32_t tfee = 0, + std::optional flags = std::nullopt, + std::optional seq = std::nullopt, + std::optional const& ter = std::nullopt); + AMM(Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + ter const& ter, + bool log = false); + + /** Send amm_info RPC command + */ + std::optional + ammRpcInfo( + std::optional const& account = std::nullopt, + std::optional const& ledgerIndex = std::nullopt, + std::optional const& ammID = std::nullopt, + bool useAssets = false) const; + + /** Verify the AMM balances. + */ + bool + expectBalances( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& lpt, + std::optional const& account = std::nullopt, + std::optional const& ledger_index = std::nullopt) const; + + bool + expectLPTokens(AccountID const& account, IOUAmount const& tokens) const; + + bool + expectAuctionSlot( + std::uint32_t fee, + std::uint32_t timeInterval, + IOUAmount const& price, + std::optional const& ledger_index = std::nullopt) const; + + bool + expectTradingFee(std::uint16_t fee) const; + + bool + expectAmmRpcInfo( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& balance, + std::optional const& account = std::nullopt, + std::optional const& ledger_index = std::nullopt) const; + + bool + ammExists() const; + + void + deposit( + std::optional const& account, + std::uint64_t tokens, + std::optional const& asset1InDetails = std::nullopt, + std::optional const& flags = std::nullopt, + std::optional const& ter = std::nullopt); + + void + deposit( + std::optional const& account, + STAmount const& asset1InDetails, + std::optional const& asset2InAmount = std::nullopt, + std::optional const& maxEP = std::nullopt, + std::optional const& flags = std::nullopt, + std::optional const& ter = std::nullopt); + + void + deposit( + std::optional const& account, + std::optional tokens, + std::optional const& asset1In, + std::optional const& asset2In, + std::optional const& maxEP, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter = std::nullopt); + + void + withdraw( + std::optional const& account, + std::optional const& tokens, + std::optional const& asset1OutDetails = std::nullopt, + std::optional const& flags = std::nullopt, + std::optional const& ter = std::nullopt); + + void + withdrawAll( + std::optional const& account, + std::optional const& asset1OutDetails = std::nullopt) + { + withdraw( + account, + std::nullopt, + asset1OutDetails, + tfAMMWithdrawAll, + std::nullopt); + } + + void + withdraw( + std::optional const& account, + STAmount const& asset1Out, + std::optional const& asset2Out = std::nullopt, + std::optional const& maxEP = std::nullopt, + std::optional const& ter = std::nullopt); + + void + withdraw( + std::optional const& account, + std::optional const& tokens, + std::optional const& asset1Out, + std::optional const& asset2Out, + std::optional const& maxEP, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter = std::nullopt); + + void + vote( + std::optional const& account, + std::uint32_t feeVal, + std::optional const& flags = std::nullopt, + std::optional const& seq = std::nullopt, + std::optional const& ter = std::nullopt); + + void + bid(std::optional const& account, + std::optional const& minSlotPrice = std::nullopt, + std::optional const& maxSlotPrice = std::nullopt, + std::vector const& authAccounts = {}, + std::optional const& flags = std::nullopt, + std::optional const& seq = std::nullopt, + std::optional const& ter = std::nullopt); + + AccountID const& + ammAccount() const + { + return ammAccount_; + } + + uint256 + ammID() const + { + return ammID_; + } + + Issue + lptIssue() const + { + return lptIssue_; + } +}; + +namespace amm { +Json::Value +trust( + AccountID const& account, + STAmount const& amount, + std::uint32_t flags = 0); +Json::Value +pay(Account const& account, AccountID const& to, STAmount const& amount); +} // namespace amm + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif // RIPPLE_TEST_JTX_AMM_H_INCLUDED diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp new file mode 100644 index 00000000000..ad35f69b226 --- /dev/null +++ b/src/test/jtx/impl/AMM.cpp @@ -0,0 +1,537 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2022 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 +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +AMM::AMM( + Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + bool log, + std::uint32_t tfee, + std::optional flags, + std::optional seq, + std::optional const& ter) + : env_(env) + , creatorAccount_(account) + , ammID_(ripple::calcAMMGroupHash(asset1.issue(), asset2.issue())) + , asset1_(asset1) + , asset2_(asset2) + , ter_(ter) + , log_(log) +{ + create(tfee, flags, seq); +} + +AMM::AMM( + Env& env, + Account const& account, + STAmount const& asset1, + STAmount const& asset2, + ter const& ter, + bool log) + : AMM(env, account, asset1, asset2, log, 0, std::nullopt, std::nullopt, ter) +{ +} + +void +AMM::create( + std::uint32_t tfee, + std::optional flags, + std::optional seq) +{ + Json::Value jv; + jv[jss::Account] = creatorAccount_.human(); + jv[jss::Asset1] = asset1_.getJson(JsonOptions::none); + jv[jss::Asset2] = asset2_.getJson(JsonOptions::none); + jv[jss::TradingFee] = tfee; + jv[jss::TransactionType] = jss::AMMInstanceCreate; + if (flags) + jv[jss::Flags] = *flags; + if (log_) + std::cout << jv.toStyledString(); + if (ter_ && seq) + env_(jv, *seq, *ter_); + else if (ter_) + env_(jv, *ter_); + else + env_(jv); + env_.close(); + if (!ter_) + { + if (auto const amm = getAMMSle( + *env_.current(), + calcAMMGroupHash(asset1_.issue(), asset2_.issue()))) + { + ammAccount_ = amm->getAccountID(sfAMMAccount); + lptIssue_ = ripple::calcLPTIssue(ammAccount_); + } + } +} + +std::optional +AMM::ammRpcInfo( + std::optional const& account, + std::optional const& ledgerIndex, + std::optional const& ammID, + bool useAssets) const +{ + Json::Value jv; + if (account) + jv[jss::account] = to_string(*account); + if (ledgerIndex) + jv[jss::ledger_index] = *ledgerIndex; + if (useAssets) + { + asset1_.setJson(jv[jss::asset1]); + asset2_.setJson(jv[jss::asset2]); + } + else if (ammID) + { + if (*ammID != uint256(0)) + jv[jss::amm_id] = to_string(*ammID); + } + else + jv[jss::amm_id] = to_string(ammID_); + auto jr = env_.rpc("json", "amm_info", to_string(jv)); + if (jr.isObject() && jr.isMember(jss::result) && + jr[jss::result].isMember(jss::status)) + return jr[jss::result]; + return std::nullopt; +} + +bool +AMM::expectBalances( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& lpt, + std::optional const& account, + std::optional const& ledger_index) const +{ + if (auto const amm = getAMMSle(*env_.current(), ammID_)) + { + auto const ammAccountID = amm->getAccountID(sfAMMAccount); + auto const [asset1Balance, asset2Balance] = ammPoolHolds( + *env_.current(), + ammAccountID, + asset1.issue(), + asset2.issue(), + env_.journal); + auto const lptAMMBalance = account + ? lpHolds(*env_.current(), ammAccountID, *account, env_.journal) + : amm->getFieldAmount(sfLPTokenBalance); + return asset1 == asset1Balance && asset2 == asset2Balance && + lptAMMBalance == STAmount{lpt, lptIssue_}; + } + return false; +} + +bool +AMM::expectLPTokens(AccountID const& account, IOUAmount const& expTokens) const +{ + if (auto const amm = getAMMSle(*env_.current(), ammID_)) + { + auto const ammAccountID = amm->getAccountID(sfAMMAccount); + auto const lptAMMBalance = + lpHolds(*env_.current(), ammAccountID, account, env_.journal); + return lptAMMBalance == STAmount{expTokens, lptIssue_}; + } + return false; +} + +bool +AMM::expectAuctionSlot( + std::uint32_t fee, + std::uint32_t timeInterval, + IOUAmount const& price, + std::optional const& ledger_index) const +{ + if (auto const amm = getAMMSle(*env_.current(), ammID_); + amm && amm->isFieldPresent(sfAuctionSlot)) + { + auto const& auctionSlot = + static_cast(amm->peekAtField(sfAuctionSlot)); + if (auctionSlot.isFieldPresent(sfAccount)) + { + return auctionSlot.getFieldU32(sfDiscountedFee) == fee && + timeSlot(env_.app().timeKeeper().now(), auctionSlot) == + timeInterval && + auctionSlot.getFieldAmount(sfPrice).iou() == price; + } + } + return false; +} + +bool +AMM::expectTradingFee(std::uint16_t fee) const +{ + if (auto const amm = getAMMSle(*env_.current(), ammID_); + amm && amm->getFieldU16(sfTradingFee) == fee) + return true; + return false; +} + +bool +AMM::ammExists() const +{ + return env_.current()->read(keylet::account(ammAccount_)) != nullptr && + env_.current()->read(keylet::amm( + calcAMMGroupHash(asset1_.issue(), asset2_.issue()))) != nullptr; +} + +bool +AMM::expectAmmRpcInfo( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& balance, + std::optional const& account, + std::optional const& ledger_index) const +{ + auto const jv = ammRpcInfo(account, ledger_index); + if (!jv) + return false; + return expectAmmInfo(asset1, asset2, balance, *jv); +} + +bool +AMM::expectAmmInfo( + STAmount const& asset1, + STAmount const& asset2, + IOUAmount const& balance, + Json::Value const& jv) const +{ + if (!jv.isMember(jss::Asset1) || !jv.isMember(jss::Asset2) || + !jv.isMember(jss::LPToken)) + return false; + STAmount asset1Info; + if (!amountFromJsonNoThrow(asset1Info, jv[jss::Asset1])) + return false; + STAmount asset2Info; + if (!amountFromJsonNoThrow(asset2Info, jv[jss::Asset2])) + return false; + STAmount lptBalance; + if (!amountFromJsonNoThrow(lptBalance, jv[jss::LPToken])) + return false; + // ammRpcInfo returns unordered assets + if (asset1Info.issue() != asset1.issue()) + { + auto const tmp = asset1Info; + asset1Info = asset2Info; + asset2Info = tmp; + } + return asset1 == asset1Info && asset2 == asset2Info && + lptBalance == STAmount{balance, lptIssue_}; +} + +void +AMM::deposit( + std::optional const& account, + Json::Value& jv, + std::optional const& seq) +{ + jv[jss::Account] = account ? account->human() : creatorAccount_.human(); + jv[jss::AMMID] = to_string(ammID_); + jv[jss::TransactionType] = jss::AMMDeposit; + if (log_) + std::cout << jv.toStyledString(); + if (ter_ && seq) + env_(jv, *seq, *ter_); + else if (ter_) + env_(jv, *ter_); + else + env_(jv); + env_.close(); +} + +void +AMM::deposit( + std::optional const& account, + std::uint64_t tokens, + std::optional const& asset1In, + std::optional const& flags, + std::optional const& ter) +{ + deposit( + account, + tokens, + asset1In, + std::nullopt, + std::nullopt, + flags, + std::nullopt, + ter); +} + +void +AMM::deposit( + std::optional const& account, + STAmount const& asset1In, + std::optional const& asset2In, + std::optional const& maxEP, + std::optional const& flags, + std::optional const& ter) +{ + assert(!(asset2In && maxEP)); + deposit( + account, + std::nullopt, + asset1In, + asset2In, + maxEP, + flags, + std::nullopt, + ter); +} + +void +AMM::deposit( + std::optional const& account, + std::optional tokens, + std::optional const& asset1In, + std::optional const& asset2In, + std::optional const& maxEP, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter) +{ + if (ter) + ter_ = *ter; + Json::Value jv; + if (tokens) + { + STAmount saTokens{lptIssue_, *tokens, 0}; + saTokens.setJson(jv[jss::LPToken]); + } + if (asset1In) + asset1In->setJson(jv[jss::Asset1In]); + if (asset2In) + asset2In->setJson(jv[jss::Asset2In]); + if (maxEP) + maxEP->setJson(jv[jss::EPrice]); + if (flags) + jv[jss::Flags] = *flags; + deposit(account, jv, seq); +} + +void +AMM::withdraw( + std::optional const& account, + Json::Value& jv, + std::optional const& seq, + std::optional const& ter) +{ + jv[jss::Account] = account ? account->human() : creatorAccount_.human(); + jv[jss::AMMID] = to_string(ammID_); + jv[jss::TransactionType] = jss::AMMWithdraw; + if (log_) + std::cout << jv.toStyledString(); + if (ter && seq) + env_(jv, *seq, *ter); + else if (ter) + env_(jv, *ter); + else + env_(jv); + env_.close(); +} + +void +AMM::withdraw( + std::optional const& account, + std::optional const& tokens, + std::optional const& asset1Out, + std::optional const& flags, + std::optional const& ter) +{ + withdraw( + account, + tokens, + asset1Out, + std::nullopt, + std::nullopt, + flags, + std::nullopt, + ter); +} + +void +AMM::withdraw( + std::optional const& account, + STAmount const& asset1Out, + std::optional const& asset2Out, + std::optional const& maxEP, + std::optional const& ter) +{ + assert(!(asset2Out && maxEP)); + withdraw( + account, + std::nullopt, + asset1Out, + asset2Out, + maxEP, + std::nullopt, + std::nullopt, + ter); +} + +void +AMM::withdraw( + std::optional const& account, + std::optional const& tokens, + std::optional const& asset1Out, + std::optional const& asset2Out, + std::optional const& maxEP, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter) +{ + Json::Value jv; + if (tokens) + { + STAmount saTokens{lptIssue_, *tokens, 0}; + saTokens.setJson(jv[jss::LPToken]); + } + if (asset1Out) + asset1Out->setJson(jv[jss::Asset1Out]); + if (asset2Out) + asset2Out->setJson(jv[jss::Asset2Out]); + if (maxEP) + { + STAmount const saMaxEP{*maxEP, lptIssue_}; + saMaxEP.setJson(jv[jss::EPrice]); + } + if (flags) + jv[jss::Flags] = *flags; + withdraw(account, jv, seq, ter); +} + +void +AMM::vote( + std::optional const& account, + std::uint32_t feeVal, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter) +{ + Json::Value jv; + jv[jss::Account] = account ? account->human() : creatorAccount_.human(); + jv[jss::AMMID] = to_string(ammID_); + jv[jss::FeeVal] = feeVal; + jv[jss::TransactionType] = jss::AMMVote; + if (flags) + jv[jss::Flags] = *flags; + if (log_) + std::cout << jv.toStyledString(); + if (ter && seq) + env_(jv, *seq, *ter); + else if (ter) + env_(jv, *ter); + else + env_(jv); + env_.close(); +} + +void +AMM::bid( + std::optional const& account, + std::optional const& minSlotPrice, + std::optional const& maxSlotPrice, + std::vector const& authAccounts, + std::optional const& flags, + std::optional const& seq, + std::optional const& ter) +{ + Json::Value jv; + jv[jss::Account] = account ? account->human() : creatorAccount_.human(); + jv[jss::AMMID] = to_string(ammID_); + if (minSlotPrice) + { + STAmount saTokens{lptIssue_, *minSlotPrice, 0}; + saTokens.setJson(jv[jss::MinSlotPrice]); + } + if (maxSlotPrice) + { + STAmount saTokens{lptIssue_, *maxSlotPrice, 0}; + saTokens.setJson(jv[jss::MaxSlotPrice]); + } + if (authAccounts.size() > 0) + { + Json::Value accounts(Json::arrayValue); + for (auto const& account : authAccounts) + { + Json::Value acct; + Json::Value authAcct; + acct[jss::Account] = account.human(); + authAcct[jss::AuthAccount] = acct; + accounts.append(authAcct); + } + jv[jss::AuthAccounts] = accounts; + } + if (flags) + jv[jss::Flags] = *flags; + jv[jss::TransactionType] = jss::AMMBid; + if (log_) + std::cout << jv.toStyledString(); + if (ter && seq) + env_(jv, *seq, *ter); + else if (ter) + env_(jv, *ter); + else + env_(jv); + env_.close(); +} + +namespace amm { +Json::Value +trust(AccountID const& account, STAmount const& amount, std::uint32_t flags) +{ + if (isXRP(amount)) + Throw("trust() requires IOU"); + Json::Value jv; + jv[jss::Account] = to_string(account); + jv[jss::LimitAmount] = amount.getJson(JsonOptions::none); + jv[jss::TransactionType] = jss::TrustSet; + jv[jss::Flags] = flags; + return jv; +} +Json::Value +pay(Account const& account, AccountID const& to, STAmount const& amount) +{ + Json::Value jv; + jv[jss::Account] = account.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + jv[jss::Destination] = to_string(to); + jv[jss::TransactionType] = jss::Payment; + jv[jss::Flags] = tfUniversal; + return jv; +} +} // namespace amm +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 39436e5be66..35df2ec191c 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -26,17 +26,22 @@ namespace test { namespace jtx { Json::Value -pay(Account const& account, Account const& to, AnyAmount amount) +pay(AccountID const& account, AccountID const& to, AnyAmount amount) { amount.to(to); Json::Value jv; - jv[jss::Account] = account.human(); + jv[jss::Account] = to_string(account); jv[jss::Amount] = amount.value.getJson(JsonOptions::none); - jv[jss::Destination] = to.human(); + jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; jv[jss::Flags] = tfUniversal; return jv; } +Json::Value +pay(Account const& account, Account const& to, AnyAmount amount) +{ + return pay(account.id(), to.id(), amount); +} } // namespace jtx } // namespace test diff --git a/src/test/jtx/pay.h b/src/test/jtx/pay.h index 6f713dc537b..f66075a3e80 100644 --- a/src/test/jtx/pay.h +++ b/src/test/jtx/pay.h @@ -30,6 +30,8 @@ namespace jtx { /** Create a payment. */ Json::Value +pay(AccountID const& account, AccountID const& to, AnyAmount amount); +Json::Value pay(Account const& account, Account const& to, AnyAmount amount); } // namespace jtx diff --git a/src/test/protocol/KnownFormatToGRPC_test.cpp b/src/test/protocol/KnownFormatToGRPC_test.cpp index bf49f2e3134..96f675680b8 100644 --- a/src/test/protocol/KnownFormatToGRPC_test.cpp +++ b/src/test/protocol/KnownFormatToGRPC_test.cpp @@ -57,6 +57,17 @@ class KnownFormatToGRPC_test : public beast::unit_test::suite static constexpr auto fieldTYPE_MESSAGE = google::protobuf::FieldDescriptor::Type::TYPE_MESSAGE; + // gRPC is going to be deprecated moving forward. New objects + // and transactions should not have serialization/deserialization + // and be added to the exceptions_. + hash_set exceptions_ = { + "AMM", + "AMMBid", + "AMMDeposit", + "AMMInstanceCreate", + "AMMVote", + "AMMWithdraw"}; + // Format names are CamelCase and FieldDescriptor names are snake_case. // Convert from CamelCase to snake_case. Do not be fooled by consecutive // capital letters like in NegativeUNL. @@ -840,6 +851,8 @@ class KnownFormatToGRPC_test : public beast::unit_test::suite for (auto const& item : knownFormat) { + if (exceptions_.find(item.getName()) != exceptions_.end()) + continue; if constexpr (std::is_same_v) { // Skip LedgerEntryTypes that gRPC does not currently support. diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp new file mode 100644 index 00000000000..e4b311cd216 --- /dev/null +++ b/src/test/rpc/AMMInfo_test.cpp @@ -0,0 +1,137 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2016 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 +#include +#include +#include +#include +//#include + +namespace ripple { +namespace test { + +class AMMInfo_test : public beast::unit_test::suite +{ + jtx::Account const gw; + jtx::Account const alice; + jtx::Account const carol; + jtx::IOU const USD; + + template + void + proc( + F&& cb, + std::optional> const& pool = {}, + std::optional const& lpt = {}) + { + using namespace jtx; + std::unique_ptr config = envconfig(addGrpcConfig); + std::string grpcPort = *(*config)["port_grpc"].get("port"); + Env env{*this, std::move(config)}; + + env.fund(jtx::XRP(30000), alice, carol, gw); + env.trust(USD(30000), alice); + env.trust(USD(30000), carol); + + env(pay(gw, alice, USD(30000))); + env(pay(gw, carol, USD(30000))); + + auto [asset1, asset2] = + [&]() -> std::pair { + if (pool) + return *pool; + return {10000, 10000}; + }(); + auto tokens = [&]() { + if (lpt) + return *lpt; + return IOUAmount{10000000, 0}; + }(); + AMM ammAlice(env, alice, XRP(asset1), USD(asset2)); + BEAST_EXPECT(ammAlice.expectBalances(XRP(asset1), USD(asset2), tokens)); + cb(ammAlice, env); + } + +public: + AMMInfo_test() : gw("gw"), alice("alice"), carol("carol"), USD(gw["USD"]) + { + } + void + testErrors() + { + testcase("Errors"); + + using namespace jtx; + // Invalid AMM hash + proc([&](AMM& ammAlice, Env&) { + uint256 hash{1}; + auto const jv = ammAlice.ammRpcInfo({}, {}, hash); + BEAST_EXPECT( + jv.has_value() && + (*jv)[jss::error_message] == "Account not found."); + }); + + // Invalid LP account id + proc([&](AMM& ammAlice, Env&) { + Account bogie("bogie"); + auto const jv = ammAlice.ammRpcInfo(bogie.id()); + BEAST_EXPECT( + jv.has_value() && + (*jv)[jss::error_message] == "Account malformed."); + }); + } + + void + testSimpleRpc() + { + testcase("RPC simple"); + + using namespace jtx; + proc([&](AMM& ammAlice, Env&) { + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + }); + proc([&](AMM& ammAlice, Env&) { + auto const ammID = [&]() -> std::optional { + if (auto const jv = ammAlice.ammRpcInfo({}, {}, {}, true); + jv.has_value()) + { + uint256 ammID; + if (ammID.parseHex((*jv)[jss::AMMID].asString())) + return ammID; + } + return {}; + }(); + BEAST_EXPECT(ammID.has_value() && ammAlice.ammID() == *ammID); + }); + } + + void + run() override + { + testErrors(); + testSimpleRpc(); + } +}; + +BEAST_DEFINE_TESTSUITE(AMMInfo, app, ripple); + +} // namespace test +} // namespace ripple