diff --git a/src/ripple/app/misc/AMMUtils.h b/src/ripple/app/misc/AMMUtils.h index c25503ceb9c..a77b3061bb8 100644 --- a/src/ripple/app/misc/AMMUtils.h +++ b/src/ripple/app/misc/AMMUtils.h @@ -113,6 +113,14 @@ initializeFeeAuctionVote( Issue const& lptIssue, std::uint16_t tfee); +/** Return true if the liquidity provider is the only AMM provider. + */ +bool +isOnlyLiquidityProvider( + ReadView const& view, + AccountID const& ammAccount, + AccountID const& lpAccount); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp index cf4da2c5d85..8e62721efbb 100644 --- a/src/ripple/app/misc/impl/AMMUtils.cpp +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -348,4 +348,56 @@ initializeFeeAuctionVote( auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE } +bool +isOnlyLiquidityProvider( + ReadView const& view, + AccountID const& ammAccount, + AccountID const& lpAccount) +{ + std::uint8_t nTrustLines = 0; + std::uint8_t limit = 10; + auto const root = keylet::ownerDir(ammAccount); + auto currentIndex = root; + + // Iterate over AMM owner directory objects. + // AMM Liquidity Provider has at most three + // trustlines. One for LPToken, and at most + // two trustlines for IOU: one if XRP/IOU + // AMM and two if IOU/IOU AMM. + while (limit-- <= 1) + { + auto const ownerDir = view.read(currentIndex); + if (!ownerDir) + return false; + for (auto const& key : ownerDir->getFieldV256(sfIndexes)) + { + auto const sle = view.read(keylet::child(key)); + if (!sle) + { + assert(false); + return false; + } + // Only one AMM object + if (sle->getFieldU16(sfLedgerEntryType) == ltAMM) + continue; + if (sle->getFieldU16(sfLedgerEntryType) != ltRIPPLE_STATE) + { + assert(false); + return false; + } + auto const lowLimit = sle->getFieldAmount(sfLowLimit); + auto const highLimit = sle->getFieldAmount(sfHighLimit); + if ((lowLimit.getIssuer() != lpAccount && + highLimit.getIssuer() != lpAccount)) + return false; + ++nTrustLines; + } + auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); + if (uNodeNext == 0) + return nTrustLines == 2 || nTrustLines == 3; + currentIndex = keylet::page(root, uNodeNext); + } + return nTrustLines == 2 || nTrustLines == 3; +} + } // namespace ripple diff --git a/src/ripple/app/tx/impl/AMMWithdraw.cpp b/src/ripple/app/tx/impl/AMMWithdraw.cpp index 839c3b433aa..765a0101765 100644 --- a/src/ripple/app/tx/impl/AMMWithdraw.cpp +++ b/src/ripple/app/tx/impl/AMMWithdraw.cpp @@ -310,6 +310,19 @@ AMMWithdraw::applyGuts(Sandbox& sb) auto const lpTokensWithdraw = tokensWithdraw(lpTokens, ctx_.tx[~sfLPTokenIn], ctx_.tx.getFlags()); + // Due to rounding, the LPTokenBalance of the last LP + // might not match the LP's trustline balance + if (sb.rules().enabled(fixAMMRounding) && + isOnlyLiquidityProvider(sb, ammAccountID, account_) && + withinRelativeDistance( + lpTokens, ammSle->getFieldAmount(sfLPTokenBalance), Number{1, -7})) + { + // LCOV_EXCL_START + ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); + sb.update(ammSle); + // LCOV_EXCL_STOP + } + auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_); auto const expected = ammHolds(