#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/sendmax.h>

#include <xrpl/basics/Number.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/tx/transactors/dex/AMMBid.h>
#include <xrpl/tx/transactors/dex/AMMContext.h>
#include <xrpl/tx/transactors/dex/AMMHelpers.h>
#include <xrpl/tx/transactors/dex/AMMUtils.h>

#include <boost/regex.hpp>

#include <utility>
#include <vector>

namespace xrpl {
namespace test {

/**
 * Basic tests of AMM that do not use offers.
 * Tests incorporating offers are in `AMMExtended_test`.
 */
struct AMM_test : public jtx::AMMTest
{
    // Use small Number mantissas for the life of this test.
    NumberMantissaScaleGuard const sg_{xrpl::MantissaRange::small};

private:
    static FeatureBitset
    testable_amendments()
    {
        // For now, just disable SAV entirely, which locks in the small Number
        // mantissas
        return jtx::testable_amendments() - featureSingleAssetVault - featureLendingProtocol;
    }

    void
    testInstanceCreate()
    {
        testcase("Instance Create");

        using namespace jtx;

#if NUMBERTODO
        // XRP to IOU, with featureSingleAssetVault
        testAMM(
            [&](AMM& ammAlice, Env&) {
                BEAST_EXPECT(
                    ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
            },
            {},
            0,
            {},
            {testable_amendments() | featureSingleAssetVault});
#endif

        // XRP to IOU, without featureSingleAssetVault
        testAMM(
            [&](AMM& ammAlice, Env&) {
                BEAST_EXPECT(
                    ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
            },
            {},
            0,
            {},
            {testable_amendments() - featureSingleAssetVault});

        // IOU to IOU
        testAMM(
            [&](AMM& ammAlice, Env&) {
                BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
            },
            {{USD(20'000), BTC(0.5)}});

        // IOU to IOU + transfer fee
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
            env(rate(gw, 1.25));
            env.close();
            // no transfer fee on create
            AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
            BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
            BEAST_EXPECT(expectHolding(env, alice, USD(0)));
            BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
        }

        // Require authorization is set, account is authorized
        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env(fset(gw, asfRequireAuth));
            env(trust(alice, gw["USD"](30'000), 0));
            env(trust(gw, alice["USD"](0), tfSetfAuth));
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
        }

        // Cleared global freeze
        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env.trust(USD(30'000), alice);
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env.close();
            env(fset(gw, asfGlobalFreeze));
            env.close();
            AMM ammAliceFail(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
            env(fclear(gw, asfGlobalFreeze));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
        }

        // Trading fee
        testAMM(
            [&](AMM& amm, Env&) {
                BEAST_EXPECT(amm.expectTradingFee(1'000));
                BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
            },
            std::nullopt,
            1'000);

        // Make sure asset comparison works.
        BEAST_EXPECT(
            STIssue(sfAsset, STAmount(XRP(2'000)).issue()) ==
            STIssue(sfAsset, STAmount(XRP(2'000)).issue()));
        BEAST_EXPECT(
            STIssue(sfAsset, STAmount(XRP(2'000)).issue()) !=
            STIssue(sfAsset, STAmount(USD(2'000)).issue()));
    }

    void
    testInvalidInstance()
    {
        testcase("Invalid Instance");

        using namespace jtx;

        // Can't have both XRP tokens
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, XRP(10'000), XRP(10'000), ter(temBAD_AMM_TOKENS));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Can't have both tokens the same IOU
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, USD(10'000), USD(10'000), ter(temBAD_AMM_TOKENS));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Can't have zero or negative amounts
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, XRP(0), USD(10'000), ter(temBAD_AMOUNT));
            BEAST_EXPECT(!ammAlice.ammExists());
            AMM ammAlice1(env, alice, XRP(10'000), USD(0), ter(temBAD_AMOUNT));
            BEAST_EXPECT(!ammAlice1.ammExists());
            AMM ammAlice2(env, alice, XRP(10'000), USD(-10'000), ter(temBAD_AMOUNT));
            BEAST_EXPECT(!ammAlice2.ammExists());
            AMM ammAlice3(env, alice, XRP(-10'000), USD(10'000), ter(temBAD_AMOUNT));
            BEAST_EXPECT(!ammAlice3.ammExists());
        }

        // Bad currency
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, XRP(10'000), BAD(10'000), ter(temBAD_CURRENCY));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Insufficient IOU balance
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, XRP(10'000), USD(40'000), ter(tecUNFUNDED_AMM));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Insufficient XRP balance
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(env, alice, XRP(40'000), USD(10'000), ter(tecUNFUNDED_AMM));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Invalid trading fee
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(
                env,
                alice,
                XRP(10'000),
                USD(10'000),
                false,
                65'001,
                10,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_FEE));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // AMM already exists
        testAMM([&](AMM& ammAlice, Env& env) {
            AMM ammCarol(env, carol, XRP(10'000), USD(10'000), ter(tecDUPLICATE));
        });

        // Invalid flags
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
            AMM ammAlice(
                env,
                alice,
                XRP(10'000),
                USD(10'000),
                false,
                0,
                10,
                tfWithdrawAll,
                std::nullopt,
                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(10'000),
                USD(10'000),
                false,
                0,
                10,
                std::nullopt,
                seq(1),
                std::nullopt,
                ter(terNO_ACCOUNT));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Require authorization is set
        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(gw, alice["USD"](30'000)));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecNO_AUTH));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Globally frozen
        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env(fset(gw, asfGlobalFreeze));
            env.close();
            env(trust(gw, alice["USD"](30'000)));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Individually frozen
        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env(trust(gw, alice["USD"](30'000)));
            env.close();
            env(trust(gw, alice["USD"](0), tfSetFreeze));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
            BEAST_EXPECT(!ammAlice.ammExists());
        }

        // Insufficient reserve, XRP/IOU
        {
            Env env(*this);
            auto const starting_xrp = XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4;
            env.fund(starting_xrp, gw);
            env.fund(starting_xrp, alice);
            env.trust(USD(2'000), alice);
            env.close();
            env(pay(gw, alice, USD(2'000)));
            env.close();
            env(offer(alice, XRP(101), USD(100)));
            env(offer(alice, XRP(102), USD(100)));
            AMM ammAlice(env, alice, XRP(1'000), USD(1'000), ter(tecUNFUNDED_AMM));
        }

        // Insufficient reserve, IOU/IOU
        {
            Env env(*this);
            auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 5;
            env.fund(starting_xrp, gw);
            env.fund(starting_xrp, alice);
            env.trust(USD(2'000), alice);
            env.trust(EUR(2'000), alice);
            env.close();
            env(pay(gw, alice, USD(2'000)));
            env(pay(gw, alice, EUR(2'000)));
            env.close();
            env(offer(alice, EUR(101), USD(100)));
            env(offer(alice, EUR(102), USD(100)));
            AMM ammAlice(env, alice, EUR(1'000), USD(1'000), ter(tecINSUF_RESERVE_LINE));
        }

        // Insufficient fee
        {
            Env env(*this);
            fund(env, gw, {alice}, XRP(2'000), {USD(2'000), EUR(2'000)});
            AMM ammAlice(
                env,
                alice,
                EUR(1'000),
                USD(1'000),
                false,
                0,
                ammCrtFee(env).drops() - 1,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(telINSUF_FEE_P));
        }

        // AMM with LPTokens

        // AMM with one LPToken from another AMM.
        testAMM([&](AMM& ammAlice, Env& env) {
            fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
            AMM ammAMMToken(
                env,
                alice,
                EUR(10'000),
                STAmount{ammAlice.lptIssue(), 1'000'000},
                ter(tecAMM_INVALID_TOKENS));
            AMM ammAMMToken1(
                env,
                alice,
                STAmount{ammAlice.lptIssue(), 1'000'000},
                EUR(10'000),
                ter(tecAMM_INVALID_TOKENS));
        });

        // AMM with two LPTokens from other AMMs.
        testAMM([&](AMM& ammAlice, Env& env) {
            fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
            AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
            auto const token1 = ammAlice.lptIssue();
            auto const token2 = ammAlice1.lptIssue();
            AMM ammAMMTokens(
                env,
                alice,
                STAmount{token1, 1'000'000},
                STAmount{token2, 1'000'000},
                ter(tecAMM_INVALID_TOKENS));
        });

        // Issuer has DefaultRipple disabled
        {
            Env env(*this);
            env.fund(XRP(30'000), gw);
            env(fclear(gw, asfDefaultRipple));
            AMM ammGw(env, gw, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
            env.fund(XRP(30'000), alice);
            env.trust(USD(30'000), alice);
            env(pay(gw, alice, USD(30'000)));
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
            Account const gw1("gw1");
            env.fund(XRP(30'000), gw1);
            env(fclear(gw1, asfDefaultRipple));
            env.trust(USD(30'000), gw1);
            env(pay(gw, gw1, USD(30'000)));
            auto const USD1 = gw1["USD"];
            AMM ammGwGw1(env, gw, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
            env.trust(USD1(30'000), alice);
            env(pay(gw1, alice, USD1(30'000)));
            AMM ammAlice1(env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
        }
    }

    void
    testInvalidDeposit(FeatureBitset features)
    {
        testcase("Invalid Deposit");

        using namespace jtx;

        testAMM([&](AMM& ammAlice, Env& env) {
            // Invalid flags
            ammAlice.deposit(alice, 1'000'000, std::nullopt, tfWithdrawAll, ter(temINVALID_FLAG));

            // Invalid options
            std::vector<std::tuple<
                std::optional<std::uint32_t>,
                std::optional<std::uint32_t>,
                std::optional<STAmount>,
                std::optional<STAmount>,
                std::optional<STAmount>,
                std::optional<std::uint16_t>>>
                invalidOptions = {
                    // flags, tokens, asset1In, asset2in, EPrice, tfee
                    {tfLPToken, 1'000, std::nullopt, USD(100), std::nullopt, std::nullopt},
                    {tfLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, std::nullopt},
                    {tfLPToken,
                     1'000,
                     std::nullopt,
                     std::nullopt,
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfLPToken,
                     std::nullopt,
                     USD(100),
                     std::nullopt,
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfLPToken, 1'000, XRP(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt},
                    {tfLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, 1'000},
                    {tfSingleAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
                    {tfSingleAsset,
                     std::nullopt,
                     std::nullopt,
                     USD(100),
                     std::nullopt,
                     std::nullopt},
                    {tfSingleAsset,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfSingleAsset, std::nullopt, USD(100), std::nullopt, std::nullopt, 1'000},
                    {tfTwoAsset, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
                    {tfTwoAsset,
                     std::nullopt,
                     XRP(100),
                     USD(100),
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfTwoAsset, std::nullopt, XRP(100), std::nullopt, std::nullopt, std::nullopt},
                    {tfTwoAsset, std::nullopt, XRP(100), USD(100), std::nullopt, 1'000},
                    {tfTwoAsset,
                     std::nullopt,
                     std::nullopt,
                     USD(100),
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfOneAssetLPToken,
                     1'000,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt},
                    {tfOneAssetLPToken,
                     std::nullopt,
                     XRP(100),
                     USD(100),
                     std::nullopt,
                     std::nullopt},
                    {tfOneAssetLPToken,
                     std::nullopt,
                     XRP(100),
                     std::nullopt,
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfOneAssetLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, 1'000},
                    {tfLimitLPToken, 1'000, std::nullopt, std::nullopt, std::nullopt, std::nullopt},
                    {tfLimitLPToken, 1'000, USD(100), std::nullopt, std::nullopt, std::nullopt},
                    {tfLimitLPToken, std::nullopt, USD(100), XRP(100), std::nullopt, std::nullopt},
                    {tfLimitLPToken,
                     std::nullopt,
                     XRP(100),
                     std::nullopt,
                     STAmount{USD, 1, -1},
                     1'000},
                    {tfTwoAssetIfEmpty,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     1'000},
                    {tfTwoAssetIfEmpty,
                     1'000,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt},
                    {tfTwoAssetIfEmpty,
                     std::nullopt,
                     XRP(100),
                     USD(100),
                     STAmount{USD, 1, -1},
                     std::nullopt},
                    {tfTwoAssetIfEmpty | tfLPToken,
                     std::nullopt,
                     XRP(100),
                     USD(100),
                     STAmount{USD, 1, -1},
                     std::nullopt}};
            for (auto const& it : invalidOptions)
            {
                ammAlice.deposit(
                    alice,
                    std::get<1>(it),
                    std::get<2>(it),
                    std::get<3>(it),
                    std::get<4>(it),
                    std::get<0>(it),
                    std::nullopt,
                    std::nullopt,
                    std::get<5>(it),
                    ter(temMALFORMED));
            }

            {
                // bad preflight1
                Json::Value jv = Json::objectValue;
                jv[jss::Account] = alice.human();
                jv[jss::TransactionType] = jss::AMMDeposit;
                jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none);
                jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none);
                jv[jss::Fee] = "-1";
                env(jv, ter(temBAD_FEE));
            }

            // Invalid tokens
            ammAlice.deposit(alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
            ammAlice.deposit(
                alice, IOUAmount{-1}, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            {
                Json::Value jv = Json::objectValue;
                jv[jss::Account] = alice.human();
                jv[jss::TransactionType] = jss::AMMDeposit;
                jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none);
                jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none);
                jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::none);
                jv[jss::Flags] = tfLPToken;
                env(jv, ter(temBAD_AMM_TOKENS));
            }

            // Invalid trading fee
            ammAlice.deposit(
                carol,
                std::nullopt,
                XRP(200),
                USD(200),
                std::nullopt,
                tfTwoAssetIfEmpty,
                std::nullopt,
                std::nullopt,
                10'000,
                ter(temBAD_FEE));

            // Invalid tokens - bogus currency
            {
                auto const iss1 = Issue{Currency(0xabc), gw.id()};
                auto const iss2 = Issue{Currency(0xdef), gw.id()};
                ammAlice.deposit(
                    alice,
                    1'000,
                    std::nullopt,
                    std::nullopt,
                    std::nullopt,
                    std::nullopt,
                    {{iss1, iss2}},
                    std::nullopt,
                    std::nullopt,
                    ter(terNO_AMM));
            }

            // Depositing mismatched token, invalid Asset1In.issue
            ammAlice.deposit(
                alice, GBP(100), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            // Depositing mismatched token, invalid Asset2In.issue
            ammAlice.deposit(
                alice, USD(100), GBP(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            // Depositing mismatched token, Asset1In.issue == Asset2In.issue
            ammAlice.deposit(
                alice, USD(100), USD(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            // Invalid amount value
            ammAlice.deposit(
                alice, USD(0), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
            ammAlice.deposit(
                alice, USD(-1'000), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
            ammAlice.deposit(
                alice, USD(10), std::nullopt, USD(-1), std::nullopt, ter(temBAD_AMOUNT));

            // Bad currency
            ammAlice.deposit(
                alice, BAD(100), std::nullopt, std::nullopt, std::nullopt, ter(temBAD_CURRENCY));

            // Invalid Account
            Account bad("bad");
            env.memoize(bad);
            ammAlice.deposit(
                bad,
                1'000'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                seq(1),
                std::nullopt,
                ter(terNO_ACCOUNT));

            // Invalid AMM
            ammAlice.deposit(
                alice,
                1'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                {{USD, GBP}},
                std::nullopt,
                std::nullopt,
                ter(terNO_AMM));

            // Single deposit: 100000 tokens worth of USD
            // Amount to deposit exceeds Max
            ammAlice.deposit(
                carol,
                100'000,
                USD(200),
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));

            // Single deposit: 100000 tokens worth of XRP
            // Amount to deposit exceeds Max
            ammAlice.deposit(
                carol,
                100'000,
                XRP(200),
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));

            // Deposit amount is invalid
            // Calculated amount to deposit is 98,000,000
            ammAlice.deposit(
                alice,
                USD(0),
                std::nullopt,
                STAmount{USD, 1, -1},
                std::nullopt,
                ter(tecUNFUNDED_AMM));
            // Calculated amount is 0
            ammAlice.deposit(
                alice,
                USD(0),
                std::nullopt,
                STAmount{USD, 2'000, -6},
                std::nullopt,
                ter(tecAMM_FAILED));

            // Deposit non-empty AMM
            ammAlice.deposit(
                carol, XRP(100), USD(100), std::nullopt, tfTwoAssetIfEmpty, ter(tecAMM_NOT_EMPTY));
        });

        // Tiny deposit
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                auto const enabledV1_3 = env.current()->rules().enabled(fixAMMv1_3);
                auto const err = !enabledV1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS);
                // Pre-amendment XRP deposit side is rounded to 0
                // and deposit fails.
                // Post-amendment XRP deposit side is rounded to 1
                // and deposit succeeds.
                ammAlice.deposit(carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err);
                // Pre/post-amendment LPTokens is rounded to 0 and deposit
                // fails with tecAMM_INVALID_TOKENS.
                ammAlice.deposit(
                    carol,
                    STAmount{USD, 1, -12},
                    std::nullopt,
                    std::nullopt,
                    std::nullopt,
                    ter(tecAMM_INVALID_TOKENS));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features, features - fixAMMv1_3});

        // Invalid AMM
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.withdrawAll(alice);
            ammAlice.deposit(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
        });

        // Globally frozen asset
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(fset(gw, asfGlobalFreeze));
                if (!features[featureAMMClawback])
                {
                    // If the issuer set global freeze, the holder still can
                    // deposit the other non-frozen token when AMMClawback is
                    // not enabled.
                    ammAlice.deposit(carol, XRP(100));
                }
                else
                {
                    // If the issuer set global freeze, the holder cannot
                    // deposit the other non-frozen token when AMMClawback is
                    // enabled.
                    ammAlice.deposit(
                        carol, XRP(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
                }
                ammAlice.deposit(
                    carol, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(
                    carol, XRP(100), USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Individually frozen (AMM) account
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(trust(gw, carol["USD"](0), tfSetFreeze));
                env.close();
                if (!features[featureAMMClawback])
                {
                    // Can deposit non-frozen token if AMMClawback is not
                    // enabled
                    ammAlice.deposit(carol, XRP(100));
                }
                else
                {
                    // Cannot deposit non-frozen token if the other token is
                    // frozen when AMMClawback is enabled
                    ammAlice.deposit(
                        carol, XRP(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
                }

                ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(
                    carol, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
                env(trust(gw, carol["USD"](0), tfClearFreeze));
                // Individually frozen AMM
                env(trust(
                    gw,
                    STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
                    tfSetFreeze));
                env.close();
                // Can deposit non-frozen token
                ammAlice.deposit(carol, XRP(100));
                ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(
                    carol, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Individually frozen (AMM) account with IOU/IOU AMM
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(trust(gw, carol["USD"](0), tfSetFreeze));
                env(trust(gw, carol["BTC"](0), tfSetFreeze));
                env.close();
                ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(
                    carol, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecFROZEN));
                env(trust(gw, carol["USD"](0), tfClearFreeze));
                // Individually frozen AMM
                env(trust(
                    gw,
                    STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
                    tfSetFreeze));
                env.close();
                // Cannot deposit non-frozen token
                ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
                ammAlice.deposit(
                    carol, USD(100), BTC(0.01), std::nullopt, std::nullopt, ter(tecFROZEN));
            },
            {{USD(20'000), BTC(0.5)}});

        // Deposit unauthorized token.
        {
            Env env(*this, features);
            env.fund(XRP(1000), gw, alice, bob);
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth));
            env(trust(alice, gw["USD"](20)));
            env.close();
            env(pay(gw, alice, gw["USD"](10)));
            env.close();
            env(trust(gw, bob["USD"](100)));
            env.close();

            AMM amm(env, alice, XRP(10), gw["USD"](10), ter(tesSUCCESS));
            env.close();

            if (features[featureAMMClawback])
            {
                // if featureAMMClawback is enabled, bob can not deposit XRP
                // because he's not authorized to hold the paired token
                // gw["USD"].
                amm.deposit(
                    bob, XRP(10), std::nullopt, std::nullopt, std::nullopt, ter(tecNO_AUTH));
            }
            else
            {
                amm.deposit(
                    bob, XRP(10), std::nullopt, std::nullopt, std::nullopt, ter(tesSUCCESS));
            }
        }

        // Insufficient XRP balance
        testAMM([&](AMM& ammAlice, Env& env) {
            env.fund(XRP(1'000), bob);
            env.close();
            // Adds LPT trustline
            ammAlice.deposit(bob, XRP(10));
            ammAlice.deposit(
                bob, XRP(1'000), std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM));
        });

        // Insufficient USD balance
        testAMM([&](AMM& ammAlice, Env& env) {
            fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
            env.close();
            ammAlice.deposit(
                bob, USD(1'001), std::nullopt, std::nullopt, std::nullopt, ter(tecUNFUNDED_AMM));
        });

        // Insufficient USD balance by tokens
        testAMM([&](AMM& ammAlice, Env& env) {
            fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
            env.close();
            ammAlice.deposit(
                bob,
                10'000'000,
                std::nullopt,
                std::nullopt,
                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(1'000), bob);
            env.trust(USD(100'000), bob);
            env.close();
            env(pay(gw, bob, USD(90'000)));
            env.close();
            ammAlice.deposit(
                bob,
                10'000'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecUNFUNDED_AMM));
        });

        // Insufficient reserve, XRP/IOU
        {
            Env env(*this);
            auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4;
            env.fund(XRP(10'000), gw);
            env.fund(XRP(10'000), alice);
            env.fund(starting_xrp, carol);
            env.trust(USD(2'000), alice);
            env.trust(USD(2'000), carol);
            env.close();
            env(pay(gw, alice, USD(2'000)));
            env(pay(gw, carol, USD(2'000)));
            env.close();
            env(offer(carol, XRP(100), USD(101)));
            env(offer(carol, XRP(100), USD(102)));
            AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
            ammAlice.deposit(
                carol,
                XRP(100),
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecINSUF_RESERVE_LINE));

            env(offer(carol, XRP(100), USD(103)));
            ammAlice.deposit(
                carol,
                USD(100),
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecINSUF_RESERVE_LINE));
        }

        // Insufficient reserve, IOU/IOU
        {
            Env env(*this);
            auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4;
            env.fund(XRP(10'000), gw);
            env.fund(XRP(10'000), alice);
            env.fund(starting_xrp, carol);
            env.trust(USD(2'000), alice);
            env.trust(EUR(2'000), alice);
            env.trust(USD(2'000), carol);
            env.trust(EUR(2'000), carol);
            env.close();
            env(pay(gw, alice, USD(2'000)));
            env(pay(gw, alice, EUR(2'000)));
            env(pay(gw, carol, USD(2'000)));
            env(pay(gw, carol, EUR(2'000)));
            env.close();
            env(offer(carol, XRP(100), USD(101)));
            env(offer(carol, XRP(100), USD(102)));
            AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
            ammAlice.deposit(
                carol,
                XRP(100),
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecINSUF_RESERVE_LINE));
        }

        // Invalid min
        testAMM([&](AMM& ammAlice, Env& env) {
            // min tokens can't be <= zero
            ammAlice.deposit(carol, 0, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
            ammAlice.deposit(carol, -1, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
            ammAlice.deposit(
                carol,
                0,
                XRP(100),
                USD(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_AMM_TOKENS));
            // min amounts can't be <= zero
            ammAlice.deposit(
                carol,
                1'000,
                XRP(0),
                USD(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_AMOUNT));
            ammAlice.deposit(
                carol,
                1'000,
                XRP(100),
                USD(-1),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_AMOUNT));
            // min amount bad currency
            ammAlice.deposit(
                carol,
                1'000,
                XRP(100),
                BAD(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_CURRENCY));
            // min amount bad token pair
            ammAlice.deposit(
                carol,
                1'000,
                XRP(100),
                XRP(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_AMM_TOKENS));
            ammAlice.deposit(
                carol,
                1'000,
                XRP(100),
                GBP(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(temBAD_AMM_TOKENS));
        });

        // Min deposit
        testAMM([&](AMM& ammAlice, Env& env) {
            // Equal deposit by tokens
            ammAlice.deposit(
                carol,
                1'000'000,
                XRP(1'000),
                USD(1'001),
                std::nullopt,
                tfLPToken,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));
            ammAlice.deposit(
                carol,
                1'000'000,
                XRP(1'001),
                USD(1'000),
                std::nullopt,
                tfLPToken,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));
            // Equal deposit by asset
            ammAlice.deposit(
                carol,
                100'001,
                XRP(100),
                USD(100),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));
            // Single deposit by asset
            ammAlice.deposit(
                carol,
                488'090,
                XRP(1'000),
                std::nullopt,
                std::nullopt,
                tfSingleAsset,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_FAILED));
        });

        // Equal deposit, tokens rounded to 0
        testAMM([&](AMM& amm, Env& env) {
            amm.deposit(DepositArg{.tokens = IOUAmount{1, -12}, .err = ter(tecAMM_INVALID_TOKENS)});
        });

        // Equal deposit limit, tokens rounded to 0
        testAMM(
            [&](AMM& amm, Env& env) {
                amm.deposit(
                    DepositArg{
                        .asset1In = STAmount{USD, 1, -15},
                        .asset2In = XRPAmount{1},
                        .err = ter(tecAMM_INVALID_TOKENS)});
            },
            {.pool = {{USD(1'000'000), XRP(1'000'000)}}, .features = {features - fixAMMv1_3}});
        testAMM([&](AMM& amm, Env& env) {
            amm.deposit(
                DepositArg{
                    .asset1In = STAmount{USD, 1, -15},
                    .asset2In = XRPAmount{1},
                    .err = ter(tecAMM_INVALID_TOKENS)});
        });

        // Single deposit by asset, tokens rounded to 0
        testAMM([&](AMM& amm, Env& env) {
            amm.deposit(
                DepositArg{.asset1In = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)});
        });

        // Single deposit by tokens, tokens rounded to 0
        testAMM([&](AMM& amm, Env& env) {
            amm.deposit(
                DepositArg{
                    .tokens = IOUAmount{1, -10},
                    .asset1In = STAmount{USD, 1, -15},
                    .err = ter(tecAMM_INVALID_TOKENS)});
        });

        // Single deposit with EPrice, tokens rounded to 0
        testAMM([&](AMM& amm, Env& env) {
            amm.deposit(
                DepositArg{
                    .asset1In = STAmount{USD, 1, -15},
                    .maxEP = STAmount{USD, 1, -1},
                    .err = ter(tecAMM_INVALID_TOKENS)});
        });
    }

    void
    testDeposit()
    {
        testcase("Deposit");

        auto const all = testable_amendments();
        using namespace jtx;

        // Equal deposit: 1000000 tokens, 10% of the current pool
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const baseFee = env.current()->fees().base;
            ammAlice.deposit(carol, 1'000'000);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
            // 30,000 less deposited 1,000
            BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
            // 30,000 less deposited 1,000 and 10 drops tx fee
            BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee}));
        });

        // equal asset deposit: unit test to exercise the rounding-down of
        // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
        // The LPTokens need to have 16 significant digits and a fractional part
        for (Number const& deltaLPTokens :
             {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}})
        {
            testAMM([&](AMM& ammAlice, Env& env) {
                // initial LPToken balance
                IOUAmount const initLPToken = ammAlice.getLPTokensBalance();
                IOUAmount const newLPTokens{deltaLPTokens};

                // carol performs a two-asset deposit
                ammAlice.deposit(DepositArg{.account = carol, .tokens = newLPTokens});

                IOUAmount const finalLPToken = ammAlice.getLPTokensBalance();

                // Change in behavior due to rounding down of LPTokens:
                // there is a decrease in the observed return of LPTokens --
                // Inputs Number{UINT64_C(100000'0000000001), -10} and
                // Number{UINT64_C(100000'0000000009), -10} are both rounded
                // down to 1e5
                BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5}));
                BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens);

                // fraction of newLPTokens/(existing LPToken balance). The
                // existing LPToken balance is 1e7
                Number const fr = deltaLPTokens / 1e7;

                // The below equations are based on Equation 1, 2 from XLS-30d
                // specification, Section: 2.3.1.2
                Number const deltaXRP = fr * 1e10;
                Number const deltaUSD = fr * 1e4;

                STAmount const depositUSD = STAmount{USD, deltaUSD};

                STAmount const depositXRP = STAmount{XRP, deltaXRP};

                // initial LPTokens (1e7) + newLPTokens
                BEAST_EXPECT(ammAlice.expectBalances(
                    XRP(10'000) + depositXRP,
                    USD(10'000) + depositUSD,
                    IOUAmount{1, 7} + newLPTokens));

                // 30,000 less deposited depositUSD
                BEAST_EXPECT(expectHolding(env, carol, USD(30'000) - depositUSD));
                // 30,000 less deposited depositXRP and 10 drops tx fee
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, carol, XRP(30'000) - depositXRP - txfee(env, 1)));
            });
        }

        // 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(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
        });

        // Equal limit deposit.
        // Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP.
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, USD(200), XRP(100));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
        });
        // Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP.
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, USD(100), XRP(200));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
        });

        // Single deposit: 1000 USD
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, USD(1'000));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10'000),
                STAmount{USD, UINT64_C(10'999'99999999999), -11},
                IOUAmount{10'488'088'48170151, -8}));
        });

        // Single deposit: 1000 XRP
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, XRP(1'000));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
        });

        // Single deposit: 100000 tokens worth of USD
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, 100000, USD(205));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0}));
        });

        // Single deposit: 100000 tokens worth of XRP
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.deposit(carol, 100'000, XRP(205));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'201), USD(10'000), IOUAmount{10'100'000, 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(1'000), std::nullopt, STAmount{USD, 1, -1});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10'000),
                STAmount{USD, UINT64_C(10'999'99999999999), -11},
                IOUAmount{10'488'088'48170151, -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(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 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(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0}));
        });

        // IOU to IOU + transfer fee
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
            env(rate(gw, 1.25));
            env.close();
            AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
            BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
            BEAST_EXPECT(expectHolding(env, alice, USD(0)));
            BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
            fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
            // no transfer fee on deposit
            ammAlice.deposit(carol, 10);
            BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
            BEAST_EXPECT(expectHolding(env, carol, USD(0)));
            BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
        }

        // Tiny deposits
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, IOUAmount{1, -3});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount{10'000'000'001},
                STAmount{USD, UINT64_C(10'000'000001), -6},
                IOUAmount{10'000'000'001, -3}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1, -3}));
        });
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, XRPAmount{1});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount{10'000'000'001}, USD(10'000), IOUAmount{1'000'000'000049999, -8}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{49999, -8}));
        });
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, STAmount{USD, 1, -10});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10'000),
                STAmount{USD, UINT64_C(10'000'00000000008), -11},
                IOUAmount{10'000'000'00000004, -8}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4, -8}));
        });

        // Issuer create/deposit
        for (auto const& feat : {all, all - fixAMMv1_3})
        {
            Env env(*this, feat);
            env.fund(XRP(30000), gw);
            AMM ammGw(env, gw, XRP(10'000), USD(10'000));
            BEAST_EXPECT(ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens()));
            ammGw.deposit(gw, 1'000'000);
            BEAST_EXPECT(ammGw.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
            ammGw.deposit(gw, USD(1'000));
            BEAST_EXPECT(ammGw.expectBalances(
                XRP(11'000),
                STAmount{USD, UINT64_C(11'999'99999999998), -11},
                IOUAmount{11'489'125'29307605, -8}));
        }

        // Issuer deposit
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.deposit(gw, 1'000'000);
            BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
            ammAlice.deposit(gw, USD(1'000));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(11'000),
                STAmount{USD, UINT64_C(11'999'99999999998), -11},
                IOUAmount{11'489'125'29307605, -8}));
        });

        // Min deposit
        testAMM([&](AMM& ammAlice, Env& env) {
            // Equal deposit by tokens
            ammAlice.deposit(
                carol,
                1'000'000,
                XRP(1'000),
                USD(1'000),
                std::nullopt,
                tfLPToken,
                std::nullopt,
                std::nullopt);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
        });
        testAMM([&](AMM& ammAlice, Env& env) {
            // Equal deposit by asset
            ammAlice.deposit(
                carol,
                1'000'000,
                XRP(1'000),
                USD(1'000),
                std::nullopt,
                tfTwoAsset,
                std::nullopt,
                std::nullopt);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
        });
        testAMM([&](AMM& ammAlice, Env& env) {
            // Single deposit by asset
            ammAlice.deposit(
                carol,
                488'088,
                XRP(1'000),
                std::nullopt,
                std::nullopt,
                tfSingleAsset,
                std::nullopt,
                std::nullopt);
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
        });
        testAMM([&](AMM& ammAlice, Env& env) {
            // Single deposit by asset
            ammAlice.deposit(
                carol,
                488'088,
                USD(1'000),
                std::nullopt,
                std::nullopt,
                tfSingleAsset,
                std::nullopt,
                std::nullopt);
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10'000),
                STAmount{USD, UINT64_C(10'999'99999999999), -11},
                IOUAmount{10'488'088'48170151, -8}));
        });
    }

    void
    testInvalidWithdraw()
    {
        testcase("Invalid Withdraw");

        auto const all = testable_amendments();
        using namespace jtx;

        testAMM(
            [&](AMM& ammAlice, Env& env) {
                WithdrawArg args{
                    .asset1Out = XRP(100),
                    .err = ter(tecAMM_BALANCE),
                };
                ammAlice.withdraw(args);
            },
            {{XRP(99), USD(99)}});

        testAMM(
            [&](AMM& ammAlice, Env& env) {
                WithdrawArg args{
                    .asset1Out = USD(100),
                    .err = ter(tecAMM_BALANCE),
                };
                ammAlice.withdraw(args);
            },
            {{XRP(99), USD(99)}});

        {
            Env env{*this};
            env.fund(XRP(30'000), gw, alice, bob);
            env.close();
            env(fset(gw, asfRequireAuth));
            env.close();
            env(trust(alice, gw["USD"](30'000), 0));
            env(trust(gw, alice["USD"](0), tfSetfAuth));
            // Bob trusts Gateway to owe him USD...
            env(trust(bob, gw["USD"](30'000), 0));
            // ...but Gateway does not authorize Bob to hold its USD.
            env.close();
            env(pay(gw, alice, USD(10'000)));
            env.close();
            AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
            WithdrawArg args{
                .account = bob,
                .asset1Out = USD(100),
                .err = ter(tecNO_AUTH),
            };
            ammAlice.withdraw(args);
        }

        testAMM([&](AMM& ammAlice, Env& env) {
            // Invalid flags
            ammAlice.withdraw(
                alice,
                1'000'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                tfBurnable,
                std::nullopt,
                std::nullopt,
                ter(temINVALID_FLAG));
            ammAlice.withdraw(
                alice,
                1'000'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                tfTwoAssetIfEmpty,
                std::nullopt,
                std::nullopt,
                ter(temINVALID_FLAG));

            // Invalid options
            std::vector<std::tuple<
                std::optional<std::uint32_t>,
                std::optional<STAmount>,
                std::optional<STAmount>,
                std::optional<IOUAmount>,
                std::optional<std::uint32_t>,
                NotTEC>>
                invalidOptions = {
                    // tokens, asset1Out, asset2Out, EPrice, flags, ter
                    {std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     tfSingleAsset | tfTwoAsset,
                     temMALFORMED},
                    {1'000, std::nullopt, std::nullopt, std::nullopt, tfWithdrawAll, temMALFORMED},
                    {std::nullopt,
                     USD(0),
                     XRP(100),
                     std::nullopt,
                     tfWithdrawAll | tfLPToken,
                     temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     USD(100),
                     std::nullopt,
                     tfWithdrawAll,
                     temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     tfWithdrawAll | tfOneAssetWithdrawAll,
                     temMALFORMED},
                    {std::nullopt,
                     USD(100),
                     std::nullopt,
                     std::nullopt,
                     tfWithdrawAll,
                     temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     tfOneAssetWithdrawAll,
                     temMALFORMED},
                    {1'000, std::nullopt, USD(100), std::nullopt, std::nullopt, temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     std::nullopt,
                     IOUAmount{250, 0},
                     tfWithdrawAll,
                     temMALFORMED},
                    {1'000,
                     std::nullopt,
                     std::nullopt,
                     IOUAmount{250, 0},
                     std::nullopt,
                     temMALFORMED},
                    {std::nullopt,
                     std::nullopt,
                     USD(100),
                     IOUAmount{250, 0},
                     std::nullopt,
                     temMALFORMED},
                    {std::nullopt,
                     XRP(100),
                     USD(100),
                     IOUAmount{250, 0},
                     std::nullopt,
                     temMALFORMED},
                    {1'000, XRP(100), USD(100), std::nullopt, std::nullopt, temMALFORMED},
                    {std::nullopt, XRP(100), USD(100), std::nullopt, tfWithdrawAll, temMALFORMED}};
            for (auto const& it : invalidOptions)
            {
                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,
                    std::nullopt,
                    ter(std::get<5>(it)));
            }

            // Invalid tokens
            ammAlice.withdraw(alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
            ammAlice.withdraw(
                alice, IOUAmount{-1}, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            // Mismatched token, invalid Asset1Out issue
            ammAlice.withdraw(alice, GBP(100), std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));

            // Mismatched token, invalid Asset2Out issue
            ammAlice.withdraw(alice, USD(100), GBP(100), std::nullopt, ter(temBAD_AMM_TOKENS));

            // Mismatched token, Asset1Out.issue == Asset2Out.issue
            ammAlice.withdraw(alice, USD(100), USD(100), std::nullopt, ter(temBAD_AMM_TOKENS));

            // Invalid amount value
            ammAlice.withdraw(alice, USD(0), std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
            ammAlice.withdraw(alice, USD(-100), std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
            ammAlice.withdraw(alice, USD(10), std::nullopt, IOUAmount{-1}, ter(temBAD_AMOUNT));

            // Invalid amount/token value, withdraw all tokens from one side
            // of the pool.
            ammAlice.withdraw(alice, USD(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE));
            ammAlice.withdraw(alice, XRP(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE));
            ammAlice.withdraw(
                alice,
                std::nullopt,
                USD(0),
                std::nullopt,
                std::nullopt,
                tfOneAssetWithdrawAll,
                std::nullopt,
                std::nullopt,
                ter(tecAMM_BALANCE));

            // Bad currency
            ammAlice.withdraw(alice, BAD(100), std::nullopt, std::nullopt, ter(temBAD_CURRENCY));

            // Invalid Account
            Account bad("bad");
            env.memoize(bad);
            ammAlice.withdraw(
                bad,
                1'000'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                seq(1),
                ter(terNO_ACCOUNT));

            // Invalid AMM
            ammAlice.withdraw(
                alice,
                1'000,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                std::nullopt,
                {{USD, GBP}},
                std::nullopt,
                ter(terNO_AMM));

            // Carol is not a Liquidity Provider
            ammAlice.withdraw(carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE));

            // Withdrawing from one side.
            // XRP by tokens
            ammAlice.withdraw(
                alice, IOUAmount(9'999'999'9999, -4), XRP(0), std::nullopt, ter(tecAMM_BALANCE));
            // USD by tokens
            ammAlice.withdraw(
                alice, IOUAmount(9'999'999'9, -1), USD(0), std::nullopt, ter(tecAMM_BALANCE));
            // XRP
            ammAlice.withdraw(alice, XRP(10'000), std::nullopt, std::nullopt, ter(tecAMM_BALANCE));
            // USD
            ammAlice.withdraw(
                alice,
                STAmount{USD, UINT64_C(9'999'9999999999999), -13},
                std::nullopt,
                std::nullopt,
                ter(tecAMM_BALANCE));
        });

        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Withdraw entire one side of the pool.
                // Pre-amendment:
                // Equal withdraw but due to XRP rounding
                // this results in full withdraw of XRP pool only,
                // while leaving a tiny amount in USD pool.
                // Post-amendment:
                // Most of the pool is withdrawn with remaining tiny amounts
                auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE);
                ammAlice.withdraw(
                    alice, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err);
                if (env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});

        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Similar to above with even smaller remaining amount
                // is it ok that the pool is unbalanced?
                // Withdraw entire one side of the pool.
                // Equal withdraw but due to XRP precision limit,
                // this results in full withdraw of XRP pool only,
                // while leaving a tiny amount in USD pool.
                auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE);
                ammAlice.withdraw(
                    alice, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err);
                if (env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});

        // Invalid AMM
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.withdrawAll(alice);
            ammAlice.withdraw(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
        });

        // Globally frozen asset
        testAMM([&](AMM& ammAlice, Env& env) {
            env(fset(gw, asfGlobalFreeze));
            env.close();
            // Can withdraw non-frozen token
            ammAlice.withdraw(alice, XRP(100));
            ammAlice.withdraw(alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
            ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
        });

        // Individually frozen (AMM) account
        testAMM([&](AMM& ammAlice, Env& env) {
            env(trust(gw, alice["USD"](0), tfSetFreeze));
            env.close();
            // Can withdraw non-frozen token
            ammAlice.withdraw(alice, XRP(100));
            ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
            ammAlice.withdraw(alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
            env(trust(gw, alice["USD"](0), tfClearFreeze));
            // Individually frozen AMM
            env(trust(
                gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
            // Can withdraw non-frozen token
            ammAlice.withdraw(alice, XRP(100));
            ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
            ammAlice.withdraw(alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
        });

        // 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, 1'000'000);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));

            ammAlice.withdraw(
                carol, 2'000'000, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
        });

        // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
        // to withdraw are 0.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                auto const err =
                    env.enabled(fixAMMv1_3) ? ter(tecAMM_INVALID_TOKENS) : ter(tecAMM_FAILED);
                ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{500, 0}, err);
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});

        // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
        // to withdraw are greater than the LP shares.
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, 1'000'000);
            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, 1'000'000);
            ammAlice.withdraw(
                carol, USD(1'700), std::nullopt, IOUAmount{520, 0}, ter(tecAMM_FAILED));
        });

        // Deposit/Withdraw the same amount with the trading fee
        testAMM(
            [&](AMM& ammAlice, Env&) {
                ammAlice.deposit(carol, USD(1'000));
                ammAlice.withdraw(
                    carol, USD(1'000), std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS));
            },
            std::nullopt,
            1'000);
        testAMM(
            [&](AMM& ammAlice, Env&) {
                ammAlice.deposit(carol, XRP(1'000));
                ammAlice.withdraw(
                    carol, XRP(1'000), std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS));
            },
            std::nullopt,
            1'000);

        // Deposit/Withdraw the same amount fails due to the tokens adjustment
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, STAmount{USD, 1, -6});
            ammAlice.withdraw(
                carol,
                STAmount{USD, 1, -6},
                std::nullopt,
                std::nullopt,
                ter(tecAMM_INVALID_TOKENS));
        });

        // Withdraw close to one side of the pool. Account's LP tokens
        // are rounded to all LP tokens.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                auto const err =
                    env.enabled(fixAMMv1_3) ? ter(tecINVARIANT_FAILED) : ter(tecAMM_BALANCE);
                ammAlice.withdraw(
                    alice,
                    STAmount{USD, UINT64_C(9'999'999999999999), -12},
                    std::nullopt,
                    std::nullopt,
                    err);
            },
            {.features = {all, all - fixAMMv1_3}, .noLog = true});

        // Tiny withdraw
        testAMM([&](AMM& ammAlice, Env&) {
            // XRP amount to withdraw is 0
            ammAlice.withdraw(
                alice, IOUAmount{1, -5}, std::nullopt, std::nullopt, ter(tecAMM_FAILED));
            // Calculated tokens to withdraw are 0
            ammAlice.withdraw(
                alice,
                std::nullopt,
                STAmount{USD, 1, -11},
                std::nullopt,
                ter(tecAMM_INVALID_TOKENS));
            ammAlice.deposit(carol, STAmount{USD, 1, -10});
            ammAlice.withdraw(
                carol,
                std::nullopt,
                STAmount{USD, 1, -9},
                std::nullopt,
                ter(tecAMM_INVALID_TOKENS));
            ammAlice.withdraw(
                carol, std::nullopt, XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS));
            ammAlice.withdraw(
                WithdrawArg{.tokens = IOUAmount{1, -10}, .err = ter(tecAMM_INVALID_TOKENS)});
            ammAlice.withdraw(
                WithdrawArg{
                    .asset1Out = STAmount{USD, 1, -15},
                    .asset2Out = XRPAmount{1},
                    .err = ter(tecAMM_INVALID_TOKENS)});
            ammAlice.withdraw(
                WithdrawArg{
                    .tokens = IOUAmount{1, -10},
                    .asset1Out = STAmount{USD, 1, -15},
                    .err = ter(tecAMM_INVALID_TOKENS)});
        });
    }

    void
    testWithdraw()
    {
        testcase("Withdraw");

        auto const all = testable_amendments();
        using namespace jtx;

        // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current
        // pool
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const baseFee = env.current()->fees().base.drops();
            // Single deposit of 100000 worth of tokens,
            // which is 10% of the pool. Carol is LP now.
            ammAlice.deposit(carol, 1'000'000);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
            // 30,000 less deposited 1,000
            BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
            // 30,000 less deposited 1,000 and 10 drops tx fee
            BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee}));

            // Carol withdraws all tokens
            ammAlice.withdraw(carol, 1'000'000);
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero())));
            BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
            BEAST_EXPECT(
                expectLedgerEntryRoot(env, carol, XRPAmount{30'000'000'000 - (2 * baseFee)}));
        });

        // Equal withdrawal by tokens 1000000, 10%
        // of the current pool
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, 1'000'000);
            BEAST_EXPECT(ammAlice.expectBalances(XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0}));
        });

        // Equal withdrawal with a limit. Withdraw XRP200.
        // If proportional withdraw of USD is less than 100
        // then 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(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
        });

        // Equal withdrawal with a limit. XRP100/USD100.
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, XRP(100), USD(200));
            BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
        });

        // Single withdrawal by amount XRP1000
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(alice, XRP(1'000));
                if (!env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8}));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{9'000'000'001}, USD(10'000), IOUAmount{9'486'832'98050514, -8}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});

        // Single withdrawal by tokens 10000.
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, 10'000, USD(0));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0}));
        });

        // Withdraw all tokens.
        testAMM([&](AMM& ammAlice, Env& env) {
            env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000}));
            // Can TrustSet only for AMM LP tokens
            env(trust(carol, STAmount{Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}),
                ter(tecNO_PERMISSION));
            env.close();
            ammAlice.withdrawAll(alice);
            BEAST_EXPECT(!ammAlice.ammExists());

            BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));

            // Can create AMM for the XRP/USD pair
            AMM ammCarol(env, carol, XRP(10'000), USD(10'000));
            BEAST_EXPECT(
                ammCarol.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
        });

        // Single deposit 1000USD, withdraw all tokens in USD
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.deposit(carol, USD(1'000));
            ammAlice.withdrawAll(carol, USD(0));
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 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(1'000));
            ammAlice.withdrawAll(carol, XRP(0));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount(9'090'909'091),
                STAmount{USD, UINT64_C(10'999'99999999999), -11},
                IOUAmount{10'000'000, 0}));
        });

        // Single deposit/withdraw by the same account
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Since a smaller amount might be deposited due to
                // the lp tokens adjustment, withdrawing by tokens
                // is generally preferred to withdrawing by amount.
                auto lpTokens = ammAlice.deposit(carol, USD(1'000));
                ammAlice.withdraw(carol, lpTokens, USD(0));
                lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6));
                ammAlice.withdraw(carol, lpTokens, USD(0));
                lpTokens = ammAlice.deposit(carol, XRPAmount(1));
                ammAlice.withdraw(carol, lpTokens, XRPAmount(0));
                if (!env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(
                        ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens()));
                }
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});

        // Single deposit by different accounts and then withdraw
        // in reverse.
        testAMM([&](AMM& ammAlice, Env&) {
            auto const carolTokens = ammAlice.deposit(carol, USD(1'000));
            auto const aliceTokens = ammAlice.deposit(alice, USD(1'000));
            ammAlice.withdraw(alice, aliceTokens, USD(0));
            ammAlice.withdraw(carol, carolTokens, USD(0));
            BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
            BEAST_EXPECT(ammAlice.expectLPTokens(alice, ammAlice.tokens()));
        });

        // Equal deposit 10%, withdraw all tokens
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, 1'000'000);
            ammAlice.withdrawAll(carol);
            BEAST_EXPECT(
                ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
        });

        // Equal deposit 10%, withdraw all tokens in USD
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, 1'000'000);
            ammAlice.withdrawAll(carol, USD(0));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(11'000),
                STAmount{USD, UINT64_C(9'090'909090909092), -12},
                IOUAmount{10'000'000, 0}));
        });

        // Equal deposit 10%, withdraw all tokens in XRP
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.deposit(carol, 1'000'000);
            ammAlice.withdrawAll(carol, XRP(0));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount(9'090'909'091), USD(11'000), IOUAmount{10'000'000, 0}));
        });

        // Withdraw with EPrice limit.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0});
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8}));
                if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(11'000'000'000),
                        STAmount{USD, UINT64_C(9'372'781065088757), -12},
                        IOUAmount{10'153'846'15384616, -8}));
                }
                else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(11'000'000'000),
                        STAmount{USD, UINT64_C(9'372'781065088769), -12},
                        IOUAmount{10'153'846'15384616, -8}));
                }
                else if (env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(11'000'000'000),
                        STAmount{USD, UINT64_C(9'372'78106508877), -11},
                        IOUAmount{10'153'846'15384616, -8}));
                }
                ammAlice.withdrawAll(carol);
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
            },
            {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, .noLog = true});

        // Withdraw with EPrice limit. AssetOut is 0.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                ammAlice.withdraw(carol, USD(0), std::nullopt, IOUAmount{520, 0});
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8}));
                if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(11'000),
                        STAmount{USD, UINT64_C(9'372'781065088757), -12},
                        IOUAmount{10'153'846'15384616, -8}));
                }
                else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(11'000),
                        STAmount{USD, UINT64_C(9'372'781065088769), -12},
                        IOUAmount{10'153'846'15384616, -8}));
                }
                else if (env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(11'000),
                        STAmount{USD, UINT64_C(9'372'78106508877), -11},
                        IOUAmount{10'153'846'15384616, -8}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});

        // IOU to IOU + transfer fee
        {
            Env env{*this};
            fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
            env(rate(gw, 1.25));
            env.close();
            // no transfer fee on create
            AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
            BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
            BEAST_EXPECT(expectHolding(env, alice, USD(0)));
            BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
            fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
            // no transfer fee on deposit
            ammAlice.deposit(carol, 10);
            BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
            BEAST_EXPECT(expectHolding(env, carol, USD(0)));
            BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
            // no transfer fee on withdraw
            ammAlice.withdraw(carol, 10);
            BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0}));
            BEAST_EXPECT(expectHolding(env, carol, USD(2'000)));
            BEAST_EXPECT(expectHolding(env, carol, BTC(0.05)));
        }

        // Tiny withdraw
        testAMM([&](AMM& ammAlice, Env&) {
            // By tokens
            ammAlice.withdraw(alice, IOUAmount{1, -3});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount{9'999'999'999},
                STAmount{USD, UINT64_C(9'999'999999), -6},
                IOUAmount{9'999'999'999, -3}));
        });
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Single XRP pool
                ammAlice.withdraw(alice, std::nullopt, XRPAmount{1});
                if (!env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{9'999'999'999}, USD(10'000), IOUAmount{9'999'999'9995, -4}));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {all, all - fixAMMv1_3});
        testAMM([&](AMM& ammAlice, Env&) {
            // Single USD pool
            ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10'000),
                STAmount{USD, UINT64_C(9'999'9999999999), -10},
                IOUAmount{9'999'999'99999995, -8}));
        });

        // Withdraw close to entire pool
        // Equal by tokens
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, IOUAmount{9'999'999'999, -3});
            BEAST_EXPECT(
                ammAlice.expectBalances(XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3}));
        });
        // USD by tokens
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, IOUAmount{9'999'999}, USD(0));
            BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1}));
        });
        // XRP by tokens
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, IOUAmount{9'999'900}, XRP(0));
            BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
        });
        // USD
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, STAmount{USD, UINT64_C(9'999'99999999999), -11});
            BEAST_EXPECT(ammAlice.expectBalances(
                XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9}));
        });
        // XRP
        testAMM([&](AMM& ammAlice, Env&) {
            ammAlice.withdraw(alice, XRPAmount{9'999'999'999});
            BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
        });
    }

    void
    testInvalidFeeVote()
    {
        testcase("Invalid Fee Vote");
        using namespace jtx;

        testAMM([&](AMM& ammAlice, Env& env) {
            // Invalid flags
            ammAlice.vote(
                std::nullopt,
                1'000,
                tfWithdrawAll,
                std::nullopt,
                std::nullopt,
                ter(temINVALID_FLAG));

            // Invalid fee.
            ammAlice.vote(
                std::nullopt, 1'001, std::nullopt, std::nullopt, std::nullopt, ter(temBAD_FEE));
            BEAST_EXPECT(ammAlice.expectTradingFee(0));

            // Invalid Account
            Account bad("bad");
            env.memoize(bad);
            ammAlice.vote(bad, 1'000, std::nullopt, seq(1), std::nullopt, ter(terNO_ACCOUNT));

            // Invalid AMM
            ammAlice.vote(alice, 1'000, std::nullopt, std::nullopt, {{USD, GBP}}, ter(terNO_AMM));

            // Account is not LP
            ammAlice.vote(
                carol, 1'000, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_INVALID_TOKENS));
        });

        // Invalid AMM
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.withdrawAll(alice);
            ammAlice.vote(alice, 1'000, std::nullopt, std::nullopt, std::nullopt, ter(terNO_AMM));
        });
    }

    void
    testFeeVote()
    {
        testcase("Fee Vote");
        auto const all = testable_amendments();
        using namespace jtx;

        // One vote sets fee to 1%.
        testAMM([&](AMM& ammAlice, Env& env) {
            BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
            ammAlice.vote({}, 1'000);
            BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
            // Discounted fee is 1/10 of trading fee.
            BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
        });

        auto vote = [&](AMM& ammAlice,
                        Env& env,
                        int i,
                        int fundUSD = 100'000,
                        std::uint32_t tokens = 10'000'000,
                        std::vector<Account>* accounts = nullptr) {
            Account a(std::to_string(i));
            // post-amendment the amount to deposit is slightly higher
            // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens
            // fund just one USD higher in this case, which is enough for
            // deposit to succeed
            if (env.enabled(fixAMMv1_3))
                ++fundUSD;
            fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct);
            ammAlice.deposit(a, tokens);
            ammAlice.vote(a, 50 * (i + 1));
            if (accounts)
                accounts->push_back(std::move(a));
        };

        // Eight votes fill all voting slots, set fee 0.175%.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                for (int i = 0; i < 7; ++i)
                    vote(ammAlice, env, i, 10'000);
                BEAST_EXPECT(ammAlice.expectTradingFee(175));
            },
            std::nullopt,
            0,
            std::nullopt,
            {all});

        // Eight votes fill all voting slots, set fee 0.175%.
        // New vote, same account, sets fee 0.225%
        testAMM([&](AMM& ammAlice, Env& env) {
            for (int i = 0; i < 7; ++i)
                vote(ammAlice, env, i);
            BEAST_EXPECT(ammAlice.expectTradingFee(175));
            Account const a("0");
            ammAlice.vote(a, 450);
            BEAST_EXPECT(ammAlice.expectTradingFee(225));
        });

        // Eight votes fill all voting slots, set fee 0.175%.
        // New vote, new account, higher vote weight, set higher fee 0.244%
        testAMM([&](AMM& ammAlice, Env& env) {
            for (int i = 0; i < 7; ++i)
                vote(ammAlice, env, i);
            BEAST_EXPECT(ammAlice.expectTradingFee(175));
            vote(ammAlice, env, 7, 100'000, 20'000'000);
            BEAST_EXPECT(ammAlice.expectTradingFee(244));
        });

        // Eight votes fill all voting slots, set fee 0.219%.
        // New vote, new account, higher vote weight, set smaller fee 0.206%
        testAMM([&](AMM& ammAlice, Env& env) {
            for (int i = 7; i > 0; --i)
                vote(ammAlice, env, i);
            BEAST_EXPECT(ammAlice.expectTradingFee(219));
            vote(ammAlice, env, 0, 100'000, 20'000'000);
            BEAST_EXPECT(ammAlice.expectTradingFee(206));
        });

        // Eight votes fill all voting slots. The accounts then withdraw all
        // tokens. An account sets a new fee and the previous slots are
        // deleted.
        testAMM([&](AMM& ammAlice, Env& env) {
            std::vector<Account> accounts;
            for (int i = 0; i < 7; ++i)
                vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
            BEAST_EXPECT(ammAlice.expectTradingFee(175));
            for (int i = 0; i < 7; ++i)
                ammAlice.withdrawAll(accounts[i]);
            ammAlice.deposit(carol, 10'000'000);
            ammAlice.vote(carol, 1'000);
            // The initial LP set the fee to 1000. Carol gets 50% voting
            // power, and the new fee is 500.
            BEAST_EXPECT(ammAlice.expectTradingFee(500));
        });

        // Eight votes fill all voting slots. The accounts then withdraw some
        // tokens. The new vote doesn't get the voting power but
        // the slots are refreshed and the fee is updated.
        testAMM([&](AMM& ammAlice, Env& env) {
            std::vector<Account> accounts;
            for (int i = 0; i < 7; ++i)
                vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
            BEAST_EXPECT(ammAlice.expectTradingFee(175));
            for (int i = 0; i < 7; ++i)
                ammAlice.withdraw(accounts[i], 9'000'000);
            ammAlice.deposit(carol, 1'000);
            // The vote is not added to the slots
            ammAlice.vote(carol, 1'000);
            auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
            for (std::uint32_t i = 0; i < info.size(); ++i)
                BEAST_EXPECT(info[i][jss::account] != carol.human());
            // But the slots are refreshed and the fee is changed
            BEAST_EXPECT(ammAlice.expectTradingFee(82));
        });
    }

    void
    testInvalidBid()
    {
        testcase("Invalid Bid");
        using namespace jtx;
        using namespace std::chrono;

        // burn all the LPTokens through a AMMBid transaction
        {
            Env env(*this);
            fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
            AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);

            // auction slot is owned by the creator of the AMM i.e. gw
            BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));

            // gw attempts to burn all her LPTokens through a bid transaction
            // this transaction fails because AMMBid transaction can not burn
            // all the outstanding LPTokens
            env(amm.bid({
                    .account = gw,
                    .bidMin = 1'000'000,
                }),
                ter(tecAMM_INVALID_TOKENS));
        }

        // burn all the LPTokens through a AMMBid transaction
        {
            Env env(*this);
            fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
            AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);

            // auction slot is owned by the creator of the AMM i.e. gw
            BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));

            // gw burns all but one of its LPTokens through a bid transaction
            // this transaction succeeds because the bid price is less than
            // the total outstanding LPToken balance
            env(amm.bid({
                    .account = gw,
                    .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
                }),
                ter(tesSUCCESS))
                .close();

            // gw must own the auction slot
            BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));

            // 999'999 tokens are burned, only 1 LPToken is owned by gw
            BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));

            // gw owns only 1 LPToken in its balance
            BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1);

            // gw attempts to burn the last of its LPTokens in an AMMBid
            // transaction. This transaction fails because it would burn all
            // the remaining LPTokens
            env(amm.bid({
                    .account = gw,
                    .bidMin = 1,
                }),
                ter(tecAMM_INVALID_TOKENS));
        }

        testAMM([&](AMM& ammAlice, Env& env) {
            // Invalid flags
            env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 0,
                    .flags = tfWithdrawAll,
                }),
                ter(temINVALID_FLAG));

            ammAlice.deposit(carol, 1'000'000);
            // Invalid Bid price <= 0
            for (auto bid : {0, -100})
            {
                env(ammAlice.bid({
                        .account = carol,
                        .bidMin = bid,
                    }),
                    ter(temBAD_AMOUNT));
                env(ammAlice.bid({
                        .account = carol,
                        .bidMax = bid,
                    }),
                    ter(temBAD_AMOUNT));
            }

            // Invalid Min/Max combination
            env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 200,
                    .bidMax = 100,
                }),
                ter(tecAMM_INVALID_TOKENS));

            // Invalid Account
            Account bad("bad");
            env.memoize(bad);
            env(ammAlice.bid({
                    .account = bad,
                    .bidMax = 100,
                }),
                seq(1),
                ter(terNO_ACCOUNT));

            // Account is not LP
            Account const dan("dan");
            env.fund(XRP(1'000), dan);
            env(ammAlice.bid({
                    .account = dan,
                    .bidMin = 100,
                }),
                ter(tecAMM_INVALID_TOKENS));
            env(ammAlice.bid({
                    .account = dan,
                }),
                ter(tecAMM_INVALID_TOKENS));

            // Auth account is invalid.
            env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 100,
                    .authAccounts = {bob},
                }),
                ter(terNO_ACCOUNT));

            // Invalid Assets
            env(ammAlice.bid({
                    .account = alice,
                    .bidMax = 100,
                    .assets = {{USD, GBP}},
                }),
                ter(terNO_AMM));

            // Invalid Min/Max issue
            env(ammAlice.bid({
                    .account = alice,
                    .bidMax = STAmount{USD, 100},
                }),
                ter(temBAD_AMM_TOKENS));
            env(ammAlice.bid({
                    .account = alice,
                    .bidMin = STAmount{USD, 100},
                }),
                ter(temBAD_AMM_TOKENS));
        });

        // Invalid AMM
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.withdrawAll(alice);
            env(ammAlice.bid({
                    .account = alice,
                    .bidMax = 100,
                }),
                ter(terNO_AMM));
        });

        // 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(1'000), bob, ed, bill, scott, james);
            env.close();
            ammAlice.deposit(carol, 1'000'000);
            env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 100,
                    .authAccounts = {bob, ed, bill, scott, james},
                }),
                ter(temMALFORMED));
        });

        // Bid price exceeds LP owned tokens
        testAMM([&](AMM& ammAlice, Env& env) {
            fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct);
            ammAlice.deposit(carol, 1'000'000);
            ammAlice.deposit(bob, 10);
            env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 1'000'001,
                }),
                ter(tecAMM_INVALID_TOKENS));
            env(ammAlice.bid({
                    .account = carol,
                    .bidMax = 1'000'001,
                }),
                ter(tecAMM_INVALID_TOKENS));
            env(ammAlice.bid({
                .account = carol,
                .bidMin = 1'000,
            }));
            BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
            // Slot purchase price is more than 1000 but bob only has 10 tokens
            env(ammAlice.bid({
                    .account = bob,
                }),
                ter(tecAMM_INVALID_TOKENS));
        });

        // Bid all tokens, still own the slot
        {
            Env env(*this);
            fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'000)});
            AMM amm(env, gw, XRP(10), USD(1'000));
            auto const lpIssue = amm.lptIssue();
            env.trust(STAmount{lpIssue, 100}, alice);
            env.trust(STAmount{lpIssue, 50}, bob);
            env(pay(gw, alice, STAmount{lpIssue, 100}));
            env(pay(gw, bob, STAmount{lpIssue, 50}));
            env(amm.bid({.account = alice, .bidMin = 100}));
            // Alice doesn't have any more tokens, but
            // she still owns the slot.
            env(amm.bid({
                    .account = bob,
                    .bidMax = 50,
                }),
                ter(tecAMM_FAILED));
        }
    }

    void
    testBid(FeatureBitset features)
    {
        testcase("Bid");
        using namespace jtx;
        using namespace std::chrono;

        // For now, just disable SAV entirely, which locks in the small Number
        // mantissas
        features = features - featureSingleAssetVault - featureLendingProtocol;

        // Auction slot initially is owned by AMM creator, who pays 0 price.

        // Bid 110 tokens. Pay bidMin.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                env(ammAlice.bid({.account = carol, .bidMin = 110}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
                // 110 tokens are burned.
                BEAST_EXPECT(
                    ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Bid with min/max when the pay price is less than min.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                // Bid exactly 110. Pay 110 because the pay price is < 110.
                env(ammAlice.bid({.account = carol, .bidMin = 110, .bidMax = 110}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
                BEAST_EXPECT(
                    ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
                // Bid exactly 180-200. Pay 180 because the pay price is < 180.
                env(ammAlice.bid({.account = alice, .bidMin = 180, .bidMax = 200}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
                BEAST_EXPECT(
                    ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Start bid at bidMin 110.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                // Bid, pay bidMin.
                env(ammAlice.bid({.account = carol, .bidMin = 110}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));

                fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
                ammAlice.deposit(bob, 1'000'000);
                // Bid, pay the computed price.
                env(ammAlice.bid({.account = bob}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));

                // Bid bidMax fails because the computed price is higher.
                env(ammAlice.bid({
                        .account = carol,
                        .bidMax = 120,
                    }),
                    ter(tecAMM_FAILED));
                // Bid MaxSlotPrice succeeds - pay computed price
                env(ammAlice.bid({.account = carol, .bidMax = 600}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));

                // Bid Min/MaxSlotPrice fails because the computed price is not
                // in range
                env(ammAlice.bid({
                        .account = carol,
                        .bidMin = 10,
                        .bidMax = 100,
                    }),
                    ter(tecAMM_FAILED));
                // Bid Min/MaxSlotPrice succeeds - pay computed price
                env(ammAlice.bid({.account = carol, .bidMin = 100, .bidMax = 600}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Slot states.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);

                fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
                ammAlice.deposit(bob, 1'000'000);
                if (!features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{12'000'000, 0}));
                }

                // Initial state. Pay bidMin.
                env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));

                // 1st Interval after close, price for 0th interval.
                env(ammAlice.bid({.account = bob}));
                env.close(seconds(AUCTION_SLOT_INTERVAL_DURATION + 1));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));

                // 10th Interval after close, price for 1st interval.
                env(ammAlice.bid({.account = carol}));
                env.close(seconds((10 * AUCTION_SLOT_INTERVAL_DURATION) + 1));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));

                // 20th Interval (expired) after close, price for 10th interval.
                env(ammAlice.bid({.account = bob}));
                env.close(
                    seconds((AUCTION_SLOT_TIME_INTERVALS * AUCTION_SLOT_INTERVAL_DURATION) + 1));
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{127'33875, -5}));

                // 0 Interval.
                env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110}));
                // ~321.09 tokens burnt on bidding fees.
                if (!features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2}));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{11'999'678'91, -2}));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Pool's fee 1%. Bid bidMin.
        // Auction slot owner and auth account trade at discounted fee -
        // 1/10 of the trading fee.
        // Other accounts trade at 1% fee.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                Account const dan("dan");
                Account const ed("ed");
                fund(env, gw, {bob, dan, ed}, {USD(20'000)}, Fund::Acct);
                ammAlice.deposit(bob, 1'000'000);
                ammAlice.deposit(ed, 1'000'000);
                ammAlice.deposit(carol, 500'000);
                ammAlice.deposit(dan, 500'000);
                auto ammTokens = ammAlice.getLPTokensBalance();
                env(ammAlice.bid({
                    .account = carol,
                    .bidMin = 120,
                    .authAccounts = {bob, ed},
                }));
                auto const slotPrice = IOUAmount{5'200};
                ammTokens -= slotPrice;
                BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
                if (!features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(XRP(13'000), USD(13'000), ammTokens));
                }
                else
                {
                    BEAST_EXPECT(
                        ammAlice.expectBalances(XRPAmount{13'000'000'003}, USD(13'000), ammTokens));
                }
                // Discounted trade
                for (int i = 0; i < 10; ++i)
                {
                    auto tokens = ammAlice.deposit(carol, USD(100));
                    ammAlice.withdraw(carol, tokens, USD(0));
                    tokens = ammAlice.deposit(bob, USD(100));
                    ammAlice.withdraw(bob, tokens, USD(0));
                    tokens = ammAlice.deposit(ed, USD(100));
                    ammAlice.withdraw(ed, tokens, USD(0));
                }
                // carol, bob, and ed pay ~0.99USD in fees.
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'499'00572620545), -11));
                    BEAST_EXPECT(
                        env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616195), -11));
                    BEAST_EXPECT(
                        env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'00572611841), -11));
                    // USD pool is slightly higher because of the fees.
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151419), -11), ammTokens));
                }
                else
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'499'00572620544), -11));
                    BEAST_EXPECT(
                        env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616194), -11));
                    BEAST_EXPECT(
                        env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10));
                    // USD pool is slightly higher because of the fees.
                    if (!features[fixAMMv1_3])
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRP(13'000),
                            STAmount(USD, UINT64_C(13'002'98282151422), -11),
                            ammTokens));
                    }
                    else
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRPAmount{13'000'000'003},
                            STAmount(USD, UINT64_C(13'002'98282151422), -11),
                            ammTokens));
                    }
                }
                ammTokens = ammAlice.getLPTokensBalance();
                // Trade with the fee
                for (int i = 0; i < 10; ++i)
                {
                    auto const tokens = ammAlice.deposit(dan, USD(100));
                    ammAlice.withdraw(dan, tokens, USD(0));
                }
                // dan pays ~9.94USD, which is ~10 times more in fees than
                // carol, bob, ed. the discounted fee is 10 times less
                // than the trading fee.
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(
                        env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'056722744), -9));
                    // USD pool gains more in dan's fees.
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens));
                    // Discounted fee payment
                    ammAlice.deposit(carol, USD(100));
                    ammTokens = ammAlice.getLPTokensBalance();
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877019), -11}, ammTokens));
                    env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
                    env.close();
                    // carol pays 100000 drops in fees
                    // 99900668XRP swapped in for 100USD
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'100'000'668},
                        STAmount{USD, UINT64_C(13'012'92609877019), -11},
                        ammTokens));
                }
                else
                {
                    if (!features[fixAMMv1_3])
                    {
                        BEAST_EXPECT(
                            env.balance(dan, USD) ==
                            STAmount(USD, UINT64_C(19'490'05672274399), -11));
                    }
                    else
                    {
                        BEAST_EXPECT(
                            env.balance(dan, USD) ==
                            STAmount(USD, UINT64_C(19'490'05672274398), -11));
                    }
                    // USD pool gains more in dan's fees.
                    if (!features[fixAMMv1_3])
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRP(13'000),
                            STAmount{USD, UINT64_C(13'012'92609877023), -11},
                            ammTokens));
                    }
                    else
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRPAmount{13'000'000'003},
                            STAmount{USD, UINT64_C(13'012'92609877024), -11},
                            ammTokens));
                    }
                    // Discounted fee payment
                    ammAlice.deposit(carol, USD(100));
                    ammTokens = ammAlice.getLPTokensBalance();
                    if (!features[fixAMMv1_3])
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRP(13'000),
                            STAmount{USD, UINT64_C(13'112'92609877023), -11},
                            ammTokens));
                    }
                    else
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRPAmount{13'000'000'003},
                            STAmount{USD, UINT64_C(13'112'92609877024), -11},
                            ammTokens));
                    }
                    env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
                    env.close();
                    // carol pays 100000 drops in fees
                    // 99900668XRP swapped in for 100USD
                    if (!features[fixAMMv1_3])
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRPAmount{13'100'000'668},
                            STAmount{USD, UINT64_C(13'012'92609877023), -11},
                            ammTokens));
                    }
                    else
                    {
                        BEAST_EXPECT(ammAlice.expectBalances(
                            XRPAmount{13'100'000'671},
                            STAmount{USD, UINT64_C(13'012'92609877024), -11},
                            ammTokens));
                    }
                }
                // Payment with the trading fee
                env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110)));
                env.close();
                // alice pays ~1.011USD in fees, which is ~10 times more
                // than carol's fee
                // 100.099431529USD swapped in for 100XRP
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'668},
                        STAmount{USD, UINT64_C(13'114'03663047264), -11},
                        ammTokens));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'668},
                        STAmount{USD, UINT64_C(13'114'03663047269), -11},
                        ammTokens));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'671},
                        STAmount{USD, UINT64_C(13'114'03663044937), -11},
                        ammTokens));
                }
                // Auction slot expired, no discounted fee
                env.close(seconds(TOTAL_TIME_SLOT_SECS + 1));
                // clock is parent's based
                env.close();
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'399'00572620545), -11));
                }
                else if (!features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'399'00572620544), -11));
                }
                ammTokens = ammAlice.getLPTokensBalance();
                for (int i = 0; i < 10; ++i)
                {
                    auto const tokens = ammAlice.deposit(carol, USD(100));
                    ammAlice.withdraw(carol, tokens, USD(0));
                }
                // carol pays ~9.94USD in fees, which is ~10 times more in
                // trading fees vs discounted fee.
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'389'06197177128), -11));
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'668},
                        STAmount{USD, UINT64_C(13'123'98038490681), -11},
                        ammTokens));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'389'06197177124), -11));
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'668},
                        STAmount{USD, UINT64_C(13'123'98038490689), -11},
                        ammTokens));
                }
                else
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'389'06197177129), -11));
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount{13'000'000'671},
                        STAmount{USD, UINT64_C(13'123'98038488352), -11},
                        ammTokens));
                }
                env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
                env.close();
                // carol pays ~1.008XRP in trading fee, which is
                // ~10 times more than the discounted fee.
                // 99.815876XRP is swapped in for 100USD
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(13'100'824'790),
                        STAmount{USD, UINT64_C(13'023'98038490681), -11},
                        ammTokens));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(13'100'824'790),
                        STAmount{USD, UINT64_C(13'023'98038490689), -11},
                        ammTokens));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(13'100'824'793),
                        STAmount{USD, UINT64_C(13'023'98038488352), -11},
                        ammTokens));
                }
            },
            std::nullopt,
            1'000,
            std::nullopt,
            {features});

        // Bid tiny amount
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Bid a tiny amount
                auto const tiny = Number{STAmount::cMinValue, STAmount::cMinOffset};
                env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{tiny}}));
                // Auction slot purchase price is equal to the tiny amount
                // since the minSlotPrice is 0 with no trading fee.
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
                // The purchase price is too small to affect the total tokens
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
                // Bid the tiny amount
                env(ammAlice.bid({
                    .account = alice,
                    .bidMin = IOUAmount{STAmount::cMinValue, STAmount::cMinOffset},
                }));
                // Pay slightly higher price
                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
                // The purchase price is still too small to affect the total
                // tokens
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Reset auth account
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(ammAlice.bid({
                    .account = alice,
                    .bidMin = IOUAmount{100},
                    .authAccounts = {carol},
                }));
                BEAST_EXPECT(ammAlice.expectAuctionSlot({carol}));
                env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}}));
                BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
                Account bob("bob");
                Account dan("dan");
                fund(env, {bob, dan}, XRP(1'000));
                env(ammAlice.bid({
                    .account = alice,
                    .bidMin = IOUAmount{100},
                    .authAccounts = {bob, dan},
                }));
                BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Bid all tokens, still own the slot and trade at a discount
        {
            Env env(*this, features);
            fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
            AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
            auto const lpIssue = amm.lptIssue();
            env.trust(STAmount{lpIssue, 500}, alice);
            env.trust(STAmount{lpIssue, 50}, bob);
            env(pay(gw, alice, STAmount{lpIssue, 500}));
            env(pay(gw, bob, STAmount{lpIssue, 50}));
            // Alice doesn't have anymore lp tokens
            env(amm.bid({.account = alice, .bidMin = 500}));
            BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
            BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0}));
            // But trades with the discounted fee since she still owns the slot.
            // Alice pays 10011 drops in fees
            env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11)));
            BEAST_EXPECT(amm.expectBalances(
                XRPAmount{1'010'010'011}, USD(1'000), IOUAmount{1'004'487'562112089, -9}));
            // Bob pays the full fee ~0.1USD
            env(pay(bob, alice, XRP(10)), path(~XRP), sendmax(USD(11)));
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(amm.expectBalances(
                    XRPAmount{1'000'010'011},
                    STAmount{USD, UINT64_C(1'010'10090898081), -11},
                    IOUAmount{1'004'487'562112089, -9}));
            }
            else
            {
                BEAST_EXPECT(amm.expectBalances(
                    XRPAmount{1'000'010'011},
                    STAmount{USD, UINT64_C(1'010'100908980811), -12},
                    IOUAmount{1'004'487'562112089, -9}));
            }
        }

        // preflight tests
        {
            Env env(*this, features);
            auto const baseFee = env.current()->fees().base;

            fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
            AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
            Json::Value tx = amm.bid({.account = alice, .bidMin = 500});

            {
                auto jtx = env.jt(tx, seq(1), fee(baseFee));
                env.app().config().features.erase(featureAMM);
                PreflightContext pfCtx(
                    env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
                auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
                BEAST_EXPECT(pf == temDISABLED);
                env.app().config().features.insert(featureAMM);
            }

            {
                auto jtx = env.jt(tx, seq(1), fee(baseFee));
                jtx.jv["TxnSignature"] = "deadbeef";
                jtx.stx = env.ust(jtx);
                PreflightContext pfCtx(
                    env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
                auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
                BEAST_EXPECT(!isTesSuccess(pf));
            }

            {
                auto jtx = env.jt(tx, seq(1), fee(baseFee));
                jtx.jv["Asset2"]["currency"] = "XRP";
                jtx.jv["Asset2"].removeMember("issuer");
                jtx.stx = env.ust(jtx);
                PreflightContext pfCtx(
                    env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
                auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
                BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
            }
        }
    }

    void
    testInvalidAMMPayment()
    {
        testcase("Invalid AMM Payment");
        using namespace jtx;
        using namespace std::chrono;
        using namespace std::literals::chrono_literals;

        // Can't pay into AMM account.
        // Can't pay out since there is no keys
        for (auto const& acct : {gw, alice})
        {
            {
                Env env(*this);
                fund(env, gw, {alice, carol}, XRP(1'000), {USD(100)});
                // XRP balance is below reserve
                AMM ammAlice(env, acct, XRP(10), USD(10));
                // Pay below reserve
                env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION));
                // Pay above reserve
                env(pay(carol, ammAlice.ammAccount(), XRP(300)), ter(tecNO_PERMISSION));
                // Pay IOU
                env(pay(carol, ammAlice.ammAccount(), USD(10)), ter(tecNO_PERMISSION));
            }
            {
                Env env(*this);
                fund(env, gw, {alice, carol}, XRP(10'000'000), {USD(10'000)});
                // XRP balance is above reserve
                AMM ammAlice(env, acct, XRP(1'000'000), USD(100));
                // Pay below reserve
                env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION));
                // Pay above reserve
                env(pay(carol, ammAlice.ammAccount(), XRP(1'000'000)), ter(tecNO_PERMISSION));
            }
        }

        // Can't pay into AMM with escrow.
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const baseFee = env.current()->fees().base;
            env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)),
                escrow::condition(escrow::cb1),
                escrow::finish_time(env.now() + 1s),
                escrow::cancel_time(env.now() + 2s),
                fee(baseFee * 150),
                ter(tecNO_PERMISSION));
        });

        // Can't pay into AMM with paychan.
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const pk = carol.pk();
            auto const settleDelay = 100s;
            NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 200s;
            env(paychan::create(
                    carol, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
                ter(tecNO_PERMISSION));
        });

        // Can't pay into AMM with checks.
        testAMM([&](AMM& ammAlice, Env& env) {
            env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)),
                ter(tecNO_PERMISSION));
        });

        // Pay amounts close to one side of the pool
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // Can't consume whole pool
                env(pay(alice, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                env(pay(alice, carol, XRP(100)),
                    path(~XRP),
                    sendmax(USD(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                // Overflow
                env(pay(alice, carol, STAmount{USD, UINT64_C(99'999999999), -9}),
                    path(~USD),
                    sendmax(XRP(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                env(pay(alice, carol, STAmount{USD, UINT64_C(999'99999999), -8}),
                    path(~USD),
                    sendmax(XRP(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                env(pay(alice, carol, STAmount{xrpIssue(), 99'999'999}),
                    path(~XRP),
                    sendmax(USD(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                // Sender doesn't have enough funds
                env(pay(alice, carol, USD(99.99)),
                    path(~USD),
                    sendmax(XRP(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
                env(pay(alice, carol, STAmount{xrpIssue(), 99'990'000}),
                    path(~XRP),
                    sendmax(USD(1'000'000'000)),
                    ter(tecPATH_PARTIAL));
            },
            {{XRP(100), USD(100)}});

        // Globally frozen
        testAMM([&](AMM& ammAlice, Env& env) {
            env(fset(gw, asfGlobalFreeze));
            env.close();
            env(pay(alice, carol, USD(1)),
                path(~USD),
                txflags(tfPartialPayment | tfNoRippleDirect),
                sendmax(XRP(10)),
                ter(tecPATH_DRY));
            env(pay(alice, carol, XRP(1)),
                path(~XRP),
                txflags(tfPartialPayment | tfNoRippleDirect),
                sendmax(USD(10)),
                ter(tecPATH_DRY));
        });

        // Individually frozen AMM
        testAMM([&](AMM& ammAlice, Env& env) {
            env(trust(
                gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
            env.close();
            env(pay(alice, carol, USD(1)),
                path(~USD),
                txflags(tfPartialPayment | tfNoRippleDirect),
                sendmax(XRP(10)),
                ter(tecPATH_DRY));
            env(pay(alice, carol, XRP(1)),
                path(~XRP),
                txflags(tfPartialPayment | tfNoRippleDirect),
                sendmax(USD(10)),
                ter(tecPATH_DRY));
        });

        // Individually frozen accounts
        testAMM([&](AMM& ammAlice, Env& env) {
            env(trust(gw, carol["USD"](0), tfSetFreeze));
            env(trust(gw, alice["USD"](0), tfSetFreeze));
            env.close();
            env(pay(alice, carol, XRP(1)),
                path(~XRP),
                sendmax(USD(10)),
                txflags(tfNoRippleDirect | tfPartialPayment),
                ter(tecPATH_DRY));
        });
    }

    void
    testBasicPaymentEngine(FeatureBitset features)
    {
        testcase("Basic Payment");
        using namespace jtx;

        // For now, just disable SAV entirely, which locks in the small Number
        // mantissas
        features =
            features - featureSingleAssetVault - featureLendingProtocol - featureLendingProtocol;

        // Payment 100USD for 100XRP.
        // Force one path with tfNoRippleDirect.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(jtx::XRP(30'000), bob);
                env.close();
                env(pay(bob, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(100)),
                    txflags(tfNoRippleDirect));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
                // Initial balance 30,000 + 100
                BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
                // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
            },
            {{XRP(10'000), USD(10'100)}},
            0,
            std::nullopt,
            {features});

        // Payment 100USD for 100XRP, use default path.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(jtx::XRP(30'000), bob);
                env.close();
                env(pay(bob, carol, USD(100)), sendmax(XRP(100)));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
                // Initial balance 30,000 + 100
                BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
                // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
            },
            {{XRP(10'000), USD(10'100)}},
            0,
            std::nullopt,
            {features});

        // 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(30'000), bob);
                env.close();
                env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
                // Initial balance 30,000 + 100
                BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
                // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
            },
            {{XRP(10'000), USD(10'100)}},
            0,
            std::nullopt,
            {features});

        // Payment with limitQuality set.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(jtx::XRP(30'000), bob);
                env.close();
                // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
                // would have been sent has it not been for limitQuality.
                env(pay(bob, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(100)),
                    txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
                // Initial balance 30,000 + 10(limited by limitQuality)
                BEAST_EXPECT(expectHolding(env, carol, USD(30'010)));
                // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
                // fee)
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));

                // Fails because of limitQuality. Would have sent
                // ~98.91USD/110XRP has it not been for limitQuality.
                env(pay(bob, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(100)),
                    txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
                    ter(tecPATH_DRY));
                env.close();
            },
            {{XRP(10'000), USD(10'010)}},
            0,
            std::nullopt,
            {features});

        // Payment with limitQuality and transfer fee set.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(rate(gw, 1.1));
                env.close();
                env.fund(jtx::XRP(30'000), bob);
                env.close();
                // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
                // would have been sent has it not been for limitQuality and
                // the transfer fee.
                env(pay(bob, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(110)),
                    txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
                // 10USD - 10% transfer fee
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{USD, UINT64_C(30'009'09090909091), -11}));
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
            },
            {{XRP(10'000), USD(10'010)}},
            0,
            std::nullopt,
            {features});

        // Fail when partial payment is not set.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(jtx::XRP(30'000), bob);
                env.close();
                env(pay(bob, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(100)),
                    txflags(tfNoRippleDirect),
                    ter(tecPATH_PARTIAL));
            },
            {{XRP(10'000), USD(10'000)}},
            0,
            std::nullopt,
            {features});

        // Non-default path (with AMM) has a better quality than default path.
        // The max possible liquidity is taken out of non-default
        // path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest
        // is taken from the offer.
        {
            Env env(*this, features);
            fund(env, gw, {alice, carol}, {USD(30'000), EUR(30'000)}, Fund::All);
            env.close();
            env.fund(XRP(1'000), bob);
            env.close();
            auto ammEUR_XRP = AMM(env, alice, XRP(10'000), EUR(10'000));
            auto ammUSD_EUR = AMM(env, alice, EUR(10'000), USD(10'000));
            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(10'030'082'730),
                STAmount(EUR, UINT64_C(9'970'007498125468), -12),
                ammEUR_XRP.tokens()));
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(ammUSD_EUR.expectBalances(
                    STAmount(USD, UINT64_C(9'970'097277662122), -12),
                    STAmount(EUR, UINT64_C(10'029'99250187452), -11),
                    ammUSD_EUR.tokens()));

                // fixReducedOffersV2 changes the expected results slightly.
                Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
                    ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)}
                    : Amounts{
                          XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787818), -14)};

                BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
            }
            else
            {
                BEAST_EXPECT(ammUSD_EUR.expectBalances(
                    STAmount(USD, UINT64_C(9'970'097277662172), -12),
                    STAmount(EUR, UINT64_C(10'029'99250187452), -11),
                    ammUSD_EUR.tokens()));

                // fixReducedOffersV2 changes the expected results slightly.
                Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
                    ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)}
                    : Amounts{
                          XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782840), -14)};

                BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
            }
            // Initial 30,000 + 100
            BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100}));
            // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
            BEAST_EXPECT(expectLedgerEntryRoot(
                env,
                bob,
                XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} - 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 ~49XRP/49USD. The rest is taken from the offer.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(XRP(1'000), bob);
                env.close();
                env.trust(EUR(2'000), alice);
                env.close();
                env(pay(gw, alice, EUR(1'000)));
                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(10'050'238'637),
                    STAmount(USD, UINT64_C(9'950'01249687578), -11),
                    ammAlice.tokens()));
                BEAST_EXPECT(expectOffers(
                    env,
                    alice,
                    2,
                    {{Amounts{XRPAmount(50'487'378), STAmount(EUR, UINT64_C(49'98750312422), -11)},
                      Amounts{
                          STAmount(EUR, UINT64_C(49'98750312422), -11),
                          STAmount(USD, UINT64_C(49'98750312422), -11)}}}));
                // Initial 30,000 + 99.99999999999
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
                // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
                // fee)
                BEAST_EXPECT(expectLedgerEntryRoot(
                    env,
                    bob,
                    XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} - txfee(env, 1)));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // 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();
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(
                        ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
                    // Initial 30,000 + 200
                    BEAST_EXPECT(expectHolding(env, carol, USD(30'200)));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'100),
                        STAmount(USD, UINT64_C(10'000'00000000001), -11),
                        ammAlice.tokens()));
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount(USD, UINT64_C(30'199'99999999999), -11)));
                }
                // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
                // - 100(offer) - 10(tx fee) - one reserve
                BEAST_EXPECT(expectLedgerEntryRoot(
                    env,
                    alice,
                    XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) -
                        txfee(env, 1)));
                BEAST_EXPECT(expectOffers(env, bob, 0));
            },
            {{XRP(10'000), USD(10'100)}},
            0,
            std::nullopt,
            {features});

        // Default path with AMM and Order Book offer.
        // Order Book offer is consumed first.
        // Remaining amount is consumed by AMM.
        {
            Env env(*this, features);
            fund(env, gw, {alice, bob, carol}, XRP(20'000), {USD(2'000)});
            env.close();
            env(offer(bob, XRP(50), USD(150)), txflags(tfPassive));
            env.close();
            AMM ammAlice(env, alice, XRP(1'000), USD(1'050));
            env(pay(alice, carol, USD(200)), sendmax(XRP(200)), txflags(tfPartialPayment));
            env.close();
            BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), USD(1'000), ammAlice.tokens()));
            BEAST_EXPECT(expectHolding(env, carol, USD(2'200)));
            BEAST_EXPECT(expectOffers(env, bob, 0));
        }

        // Offer crossing XRP/IOU
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
                env.close();
                env(offer(bob, USD(100), XRP(100)));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
                // Initial 1,000 + 100
                BEAST_EXPECT(expectHolding(env, bob, USD(1'100)));
                // Initial 30,000 - 100(offer) - 10(tx fee)
                BEAST_EXPECT(
                    expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
                BEAST_EXPECT(expectOffers(env, bob, 0));
            },
            {{XRP(10'000), USD(10'100)}},
            0,
            std::nullopt,
            {features});

        // Offer crossing IOU/IOU and transfer rate
        // Single path AMM offer
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(rate(gw, 1.25));
                env.close();
                // This offer succeeds to cross pre- and post-amendment
                // because the strand's out amount is small enough to match
                // limitQuality value and limitOut() function in StrandFlow
                // doesn't require an adjustment to out value.
                env(offer(carol, EUR(100), GBP(100)));
                env.close();
                // No transfer fee
                BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
                // Initial 30,000 - 100(offer) - 25% transfer fee
                BEAST_EXPECT(expectHolding(env, carol, GBP(29'875)));
                // Initial 30,000 + 100(offer)
                BEAST_EXPECT(expectHolding(env, carol, EUR(30'100)));
                BEAST_EXPECT(expectOffers(env, bob, 0));
            },
            {{GBP(1'000), EUR(1'100)}},
            0,
            std::nullopt,
            {features});
        // Single-path AMM offer
        testAMM(
            [&](AMM& amm, Env& env) {
                env(rate(gw, 1.001));
                env.close();
                env(offer(carol, XRP(100), USD(55)));
                env.close();
                if (!features[fixAMMv1_1])
                {
                    // Pre-amendment the transfer fee is not taken into
                    // account when calculating the limit out based on
                    // limitQuality. Carol pays 0.1% on the takerGets, which
                    // lowers the overall quality. AMM offer is generated based
                    // on higher limit out, which generates a larger offer
                    // with lower quality. Consequently, the offer fails
                    // to cross.
                    BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
                    BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
                }
                else
                {
                    // Post-amendment the transfer fee is taken into account
                    // when calculating the limit out based on limitQuality.
                    // This increases the limitQuality and decreases
                    // the limit out. Consequently, AMM offer size is decreased,
                    // and the quality is increased, matching the overall
                    // quality.
                    // AMM offer ~50USD/91XRP
                    BEAST_EXPECT(amm.expectBalances(
                        XRPAmount(909'090'909),
                        STAmount{USD, UINT64_C(550'000000055), -9},
                        amm.tokens()));
                    // Offer ~91XRP/49.99USD
                    BEAST_EXPECT(expectOffers(
                        env,
                        carol,
                        1,
                        {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
                    // Carol pays 0.1% fee on ~50USD =~ 0.05USD
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(29'949'94999999494), -11));
                }
            },
            {{XRP(1'000), USD(500)}},
            0,
            std::nullopt,
            {features});
        testAMM(
            [&](AMM& amm, Env& env) {
                env(rate(gw, 1.001));
                env.close();
                env(offer(carol, XRP(10), USD(5.5)));
                env.close();
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(amm.expectBalances(
                        XRP(990), STAmount{USD, UINT64_C(505'050505050505), -12}, amm.tokens()));
                    BEAST_EXPECT(expectOffers(env, carol, 0));
                }
                else
                {
                    BEAST_EXPECT(amm.expectBalances(
                        XRP(990), STAmount{USD, UINT64_C(505'0505050505051), -13}, amm.tokens()));
                    BEAST_EXPECT(expectOffers(env, carol, 0));
                }
            },
            {{XRP(1'000), USD(500)}},
            0,
            std::nullopt,
            {features});
        // Multi-path AMM offer
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                Account const ed("ed");
                fund(env, gw, {bob, ed}, XRP(30'000), {GBP(2'000), EUR(2'000)}, Fund::Acct);
                env(rate(gw, 1.25));
                env.close();
                // The auto-bridge is worse quality than AMM, is not consumed
                // first and initially forces multi-path AMM offer generation.
                // Multi-path AMM offers are consumed until their quality
                // is less than the auto-bridge offers quality. Auto-bridge
                // offers are consumed afterward. Then the behavior is
                // different pre-amendment and post-amendment.
                env(offer(bob, GBP(10), XRP(10)), txflags(tfPassive));
                env(offer(ed, XRP(10), EUR(10)), txflags(tfPassive));
                env.close();
                env(offer(carol, EUR(100), GBP(100)));
                env.close();
                if (!features[fixAMMv1_1])
                {
                    // After the auto-bridge offers are consumed, single path
                    // AMM offer is generated with the limit out not taking
                    // into consideration the transfer fee. This results
                    // in an overall lower quality offer than the limit quality
                    // and the single path AMM offer fails to consume.
                    // Total consumed ~37.06GBP/39.32EUR
                    BEAST_EXPECT(ammAlice.expectBalances(
                        STAmount{GBP, UINT64_C(1'037'06583722133), -11},
                        STAmount{EUR, UINT64_C(1'060'684828792831), -12},
                        ammAlice.tokens()));
                    // Consumed offer ~49.32EUR/49.32GBP
                    BEAST_EXPECT(expectOffers(
                        env,
                        carol,
                        1,
                        {Amounts{
                            STAmount{EUR, UINT64_C(50'684828792831), -12},
                            STAmount{GBP, UINT64_C(50'684828792831), -12}}}));
                    BEAST_EXPECT(expectOffers(env, bob, 0));
                    BEAST_EXPECT(expectOffers(env, ed, 0));

                    // Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB))
                    // * 1.25
                    //     = 58.825 = ~29941.17
                    // carol bought ~72.93EUR at the cost of ~70.68GBP
                    // the offer is partially consumed
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{GBP, UINT64_C(29'941'16770347333), -11}));
                    // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB))
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{EUR, UINT64_C(30'049'31517120716), -11}));
                }
                else
                {
                    // After the auto-bridge offers are consumed, single path
                    // AMM offer is generated with the limit out taking
                    // into consideration the transfer fee. This results
                    // in an overall quality offer matching the limit quality
                    // and the single path AMM offer is consumed. More
                    // liquidity is consumed overall in post-amendment.
                    // Total consumed ~60.68GBP/62.93EUR
                    BEAST_EXPECT(ammAlice.expectBalances(
                        STAmount{GBP, UINT64_C(1'060'684828792832), -12},
                        STAmount{EUR, UINT64_C(1'037'06583722134), -11},
                        ammAlice.tokens()));
                    // Consumed offer ~72.93EUR/72.93GBP
                    BEAST_EXPECT(expectOffers(
                        env,
                        carol,
                        1,
                        {Amounts{
                            STAmount{EUR, UINT64_C(27'06583722134028), -14},
                            STAmount{GBP, UINT64_C(27'06583722134028), -14}}}));
                    BEAST_EXPECT(expectOffers(env, bob, 0));
                    BEAST_EXPECT(expectOffers(env, ed, 0));

                    // Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB))
                    // * 1.25
                    //     = 88.35 = ~29911.64
                    // carol bought ~72.93EUR at the cost of ~70.68GBP
                    // the offer is partially consumed
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{GBP, UINT64_C(29'911'64396400896), -11}));
                    // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB))
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{EUR, UINT64_C(30'072'93416277865), -11}));
                }
                // Initial 2000 + 10 = 2010
                BEAST_EXPECT(expectHolding(env, bob, GBP(2'010)));
                // Initial 2000 - 10 * 1.25 = 1987.5
                BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5)));
            },
            {{GBP(1'000), EUR(1'100)}},
            0,
            std::nullopt,
            {features});

        // Payment and transfer fee
        // Scenario:
        // Bob sends 125GBP to pay 80EUR to Carol
        // Payment execution:
        // bob's 125GBP/1.25 = 100GBP
        // 100GBP/100EUR AMM offer
        // 100EUR/1.25 = 80EUR paid to carol
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, {GBP(200), EUR(200)}, Fund::Acct);
                env(rate(gw, 1.25));
                env.close();
                env(pay(bob, carol, EUR(100)),
                    path(~EUR),
                    sendmax(GBP(125)),
                    txflags(tfPartialPayment));
                env.close();
                BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
                BEAST_EXPECT(expectHolding(env, bob, GBP(75)));
                BEAST_EXPECT(expectHolding(env, carol, EUR(30'080)));
            },
            {{GBP(1'000), EUR(1'100)}},
            0,
            std::nullopt,
            {features});

        // Payment and transfer fee, multiple steps
        // Scenario:
        // Dan's offer 200CAN/200GBP
        // AMM 1000GBP/10125EUR
        // Ed's offer 200EUR/200USD
        // Bob sends 195.3125CAN to pay 100USD to Carol
        // Payment execution:
        // bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer
        // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
        // 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer
        // 100EUR/100USD 100USD/1.25 = 80USD paid to carol
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                Account const dan("dan");
                Account const ed("ed");
                auto const CAN = gw["CAN"];
                fund(env, gw, {dan}, {CAN(200), GBP(200)}, Fund::Acct);
                fund(env, gw, {ed}, {EUR(200), USD(200)}, Fund::Acct);
                fund(env, gw, {bob}, {CAN(195.3125)}, Fund::Acct);
                env(trust(carol, USD(100)));
                env(rate(gw, 1.25));
                env.close();
                env(offer(dan, CAN(200), GBP(200)));
                env(offer(ed, EUR(200), USD(200)));
                env.close();
                env(pay(bob, carol, USD(100)),
                    path(~GBP, ~EUR, ~USD),
                    sendmax(CAN(195.3125)),
                    txflags(tfPartialPayment));
                env.close();
                BEAST_EXPECT(expectHolding(env, bob, CAN(0)));
                BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75)));
                BEAST_EXPECT(ammAlice.expectBalances(GBP(10'125), EUR(10'000), ammAlice.tokens()));
                BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100)));
                BEAST_EXPECT(expectHolding(env, carol, USD(80)));
            },
            {{GBP(10'000), EUR(10'125)}},
            0,
            std::nullopt,
            {features});

        // Pay amounts close to one side of the pool
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(pay(alice, carol, USD(99.99)),
                    path(~USD),
                    sendmax(XRP(1)),
                    txflags(tfPartialPayment),
                    ter(tesSUCCESS));
                env(pay(alice, carol, USD(100)),
                    path(~USD),
                    sendmax(XRP(1)),
                    txflags(tfPartialPayment),
                    ter(tesSUCCESS));
                env(pay(alice, carol, XRP(100)),
                    path(~XRP),
                    sendmax(USD(1)),
                    txflags(tfPartialPayment),
                    ter(tesSUCCESS));
                env(pay(alice, carol, STAmount{xrpIssue(), 99'999'900}),
                    path(~XRP),
                    sendmax(USD(1)),
                    txflags(tfPartialPayment),
                    ter(tesSUCCESS));
            },
            {{XRP(100), USD(100)}},
            0,
            std::nullopt,
            {features});

        // Multiple paths/steps
        {
            Env env(*this, features);
            auto const ETH = gw["ETH"];
            fund(
                env,
                gw,
                {alice},
                XRP(100'000),
                {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
            fund(env, gw, {carol, bob}, XRP(1'000), {USD(200)}, Fund::Acct);
            AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
            AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
            AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
            AMM xrp_usd(env, alice, XRP(10'150), USD(10'200));
            AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
            AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
            AMM eur_usd(env, alice, EUR(10'100), USD(10'000));
            env(pay(bob, carol, USD(100)),
                path(~EUR, ~BTC, ~USD),
                path(~USD),
                path(~ETH, ~EUR, ~USD),
                sendmax(XRP(200)));
            if (!features[fixAMMv1_1])
            {
                // XRP-ETH-EUR-USD
                // This path provides ~26.06USD/26.2XRP
                BEAST_EXPECT(xrp_eth.expectBalances(
                    XRPAmount(10'026'208'900),
                    STAmount{ETH, UINT64_C(10'073'65779244494), -11},
                    xrp_eth.tokens()));
                BEAST_EXPECT(eth_eur.expectBalances(
                    STAmount{ETH, UINT64_C(10'926'34220755506), -11},
                    STAmount{EUR, UINT64_C(10'973'54232078752), -11},
                    eth_eur.tokens()));
                BEAST_EXPECT(eur_usd.expectBalances(
                    STAmount{EUR, UINT64_C(10'126'45767921248), -11},
                    STAmount{USD, UINT64_C(9'973'93151712086), -11},
                    eur_usd.tokens()));
                // XRP-USD path
                // This path provides ~73.9USD/74.1XRP
                BEAST_EXPECT(xrp_usd.expectBalances(
                    XRPAmount(10'224'106'246),
                    STAmount{USD, UINT64_C(10'126'06848287914), -11},
                    xrp_usd.tokens()));
            }
            else
            {
                BEAST_EXPECT(xrp_eth.expectBalances(
                    XRPAmount(10'026'208'900),
                    STAmount{ETH, UINT64_C(10'073'65779244461), -11},
                    xrp_eth.tokens()));
                BEAST_EXPECT(eth_eur.expectBalances(
                    STAmount{ETH, UINT64_C(10'926'34220755539), -11},
                    STAmount{EUR, UINT64_C(10'973'5423207872), -10},
                    eth_eur.tokens()));
                BEAST_EXPECT(eur_usd.expectBalances(
                    STAmount{EUR, UINT64_C(10'126'4576792128), -10},
                    STAmount{USD, UINT64_C(9'973'93151712057), -11},
                    eur_usd.tokens()));
                // XRP-USD path
                // This path provides ~73.9USD/74.1XRP
                BEAST_EXPECT(xrp_usd.expectBalances(
                    XRPAmount(10'224'106'246),
                    STAmount{USD, UINT64_C(10'126'06848287943), -11},
                    xrp_usd.tokens()));
            }

            // XRP-EUR-BTC-USD
            // This path doesn't provide any liquidity due to how
            // offers are generated in multi-path. Analytical solution
            // shows a different distribution:
            // XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP,
            // XRP-ETH-EUR-USD 27.6USD/27.6XRP
            BEAST_EXPECT(xrp_eur.expectBalances(XRP(10'100), EUR(10'000), xrp_eur.tokens()));
            BEAST_EXPECT(eur_btc.expectBalances(EUR(10'000), BTC(10'200), eur_btc.tokens()));
            BEAST_EXPECT(btc_usd.expectBalances(BTC(10'100), USD(10'000), btc_usd.tokens()));

            BEAST_EXPECT(expectHolding(env, carol, USD(300)));
        }

        // Dependent AMM
        {
            Env env(*this, features);
            auto const ETH = gw["ETH"];
            fund(
                env,
                gw,
                {alice},
                XRP(40'000),
                {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
            fund(env, gw, {carol, bob}, XRP(1000), {USD(200)}, Fund::Acct);
            AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
            AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
            AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
            AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
            AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
            env(pay(bob, carol, USD(100)),
                path(~EUR, ~BTC, ~USD),
                path(~ETH, ~EUR, ~BTC, ~USD),
                sendmax(XRP(200)));
            if (!features[fixAMMv1_1])
            {
                // XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP
                // XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP
                BEAST_EXPECT(xrp_eur.expectBalances(
                    XRPAmount(10'118'738'472),
                    STAmount{EUR, UINT64_C(9'981'544436337968), -12},
                    xrp_eur.tokens()));
                BEAST_EXPECT(eur_btc.expectBalances(
                    STAmount{EUR, UINT64_C(10'101'16096785173), -11},
                    STAmount{BTC, UINT64_C(10'097'91426968066), -11},
                    eur_btc.tokens()));
                BEAST_EXPECT(btc_usd.expectBalances(
                    STAmount{BTC, UINT64_C(10'202'08573031934), -11},
                    USD(9'900),
                    btc_usd.tokens()));
                BEAST_EXPECT(xrp_eth.expectBalances(
                    XRPAmount(10'082'446'397),
                    STAmount{ETH, UINT64_C(10'017'41072778012), -11},
                    xrp_eth.tokens()));
                BEAST_EXPECT(eth_eur.expectBalances(
                    STAmount{ETH, UINT64_C(10'982'58927221988), -11},
                    STAmount{EUR, UINT64_C(10'917'2945958103), -10},
                    eth_eur.tokens()));
            }
            else
            {
                BEAST_EXPECT(xrp_eur.expectBalances(
                    XRPAmount(10'118'738'472),
                    STAmount{EUR, UINT64_C(9'981'544436337923), -12},
                    xrp_eur.tokens()));
                BEAST_EXPECT(eur_btc.expectBalances(
                    STAmount{EUR, UINT64_C(10'101'16096785188), -11},
                    STAmount{BTC, UINT64_C(10'097'91426968059), -11},
                    eur_btc.tokens()));
                BEAST_EXPECT(btc_usd.expectBalances(
                    STAmount{BTC, UINT64_C(10'202'08573031941), -11},
                    USD(9'900),
                    btc_usd.tokens()));
                BEAST_EXPECT(xrp_eth.expectBalances(
                    XRPAmount(10'082'446'397),
                    STAmount{ETH, UINT64_C(10'017'41072777996), -11},
                    xrp_eth.tokens()));
                BEAST_EXPECT(eth_eur.expectBalances(
                    STAmount{ETH, UINT64_C(10'982'58927222004), -11},
                    STAmount{EUR, UINT64_C(10'917'2945958102), -10},
                    eth_eur.tokens()));
            }
            BEAST_EXPECT(expectHolding(env, carol, USD(300)));
        }

        // AMM offers limit
        // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(XRP(1'000), bob);
                fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
                env(trust(alice, EUR(200)));
                for (int i = 0; i < 30; ++i)
                    env(offer(alice, EUR(1.0 + (0.01 * i)), XRP(1)));
                // This is worse quality offer than 30 offers above.
                // It will not be consumed because of AMM offers limit.
                env(offer(alice, EUR(140), XRP(100)));
                env(pay(bob, carol, USD(100)),
                    path(~XRP, ~USD),
                    sendmax(EUR(400)),
                    txflags(tfPartialPayment | tfNoRippleDirect));
                if (!features[fixAMMv1_1])
                {
                    // Carol gets ~29.91USD because of the AMM offers limit
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'030),
                        STAmount{USD, UINT64_C(9'970'089730807577), -12},
                        ammAlice.tokens()));
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{USD, UINT64_C(30'029'91026919241), -11}));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'030),
                        STAmount{USD, UINT64_C(9'970'089730807827), -12},
                        ammAlice.tokens()));
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{USD, UINT64_C(30'029'91026919217), -11}));
                }
                BEAST_EXPECT(expectOffers(env, alice, 1, {{{EUR(140), XRP(100)}}}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});
        // This payment is fulfilled
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env.fund(XRP(1'000), bob);
                fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
                env(trust(alice, EUR(200)));
                for (int i = 0; i < 29; ++i)
                    env(offer(alice, EUR(1.0 + (0.01 * i)), XRP(1)));
                // This is worse quality offer than 30 offers above.
                // It will not be consumed because of AMM offers limit.
                env(offer(alice, EUR(140), XRP(100)));
                env(pay(bob, carol, USD(100)),
                    path(~XRP, ~USD),
                    sendmax(EUR(400)),
                    txflags(tfPartialPayment | tfNoRippleDirect));
                BEAST_EXPECT(ammAlice.expectBalances(
                    XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens()));
                if (!features[fixAMMv1_1])
                {
                    // Carol gets ~100USD
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
                }
                else
                {
                    BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
                }
                BEAST_EXPECT(expectOffers(
                    env,
                    alice,
                    1,
                    {{{STAmount{EUR, UINT64_C(39'1858572), -7}, XRPAmount{27'989'898}}}}));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Offer crossing with AMM and another offer. AMM has a better
        // quality and is consumed first.
        {
            Env env(*this, features);
            fund(env, gw, {alice, carol, bob}, XRP(30'000), {USD(30'000)});
            env(offer(bob, XRP(100), USD(100.001)));
            AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
            env(offer(carol, USD(100), XRP(100)));
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(ammAlice.expectBalances(
                    XRPAmount{10'049'825'373},
                    STAmount{USD, UINT64_C(10'049'92586949302), -11},
                    ammAlice.tokens()));
                BEAST_EXPECT(expectOffers(
                    env,
                    bob,
                    1,
                    {{{XRPAmount{50'074'629}, STAmount{USD, UINT64_C(50'07513050698), -11}}}}));
            }
            else
            {
                BEAST_EXPECT(ammAlice.expectBalances(
                    XRPAmount{10'049'825'372},
                    STAmount{USD, UINT64_C(10'049'92587049303), -11},
                    ammAlice.tokens()));
                BEAST_EXPECT(expectOffers(
                    env,
                    bob,
                    1,
                    {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}}));
                BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
            }
        }

        // Individually frozen account
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                env(trust(gw, carol["USD"](0), tfSetFreeze));
                env(trust(gw, alice["USD"](0), tfSetFreeze));
                env.close();
                env(pay(alice, carol, USD(1)),
                    path(~USD),
                    sendmax(XRP(10)),
                    txflags(tfNoRippleDirect | tfPartialPayment),
                    ter(tesSUCCESS));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});
    }

    void
    testAMMTokens()
    {
        testcase("AMM Tokens");
        using namespace jtx;

        // Offer crossing with AMM LPTokens and XRP.
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const baseFee = env.current()->fees().base.drops();
            auto const token1 = ammAlice.lptIssue();
            auto priceXRP = ammAssetOut(
                STAmount{XRPAmount{10'000'000'000}},
                STAmount{token1, 10'000'000},
                STAmount{token1, 5'000'000},
                0);
            // Carol places an order to buy LPTokens
            env(offer(carol, STAmount{token1, 5'000'000}, priceXRP));
            // Alice places an order to sell LPTokens
            env(offer(alice, priceXRP, STAmount{token1, 5'000'000}));
            // Pool's LPTokens balance doesn't change
            BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
            // Carol is Liquidity Provider
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{5'000'000}));
            BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
            // Carol votes
            ammAlice.vote(carol, 1'000);
            BEAST_EXPECT(ammAlice.expectTradingFee(500));
            ammAlice.vote(carol, 0);
            BEAST_EXPECT(ammAlice.expectTradingFee(0));
            // Carol bids
            env(ammAlice.bid({.account = carol, .bidMin = 100}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900}));
            BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
            BEAST_EXPECT(accountBalance(env, carol) == std::to_string(22500000000 - (4 * baseFee)));
            priceXRP = ammAssetOut(
                STAmount{XRPAmount{10'000'000'000}},
                STAmount{token1, 9'999'900},
                STAmount{token1, 4'999'900},
                0);
            // Carol withdraws
            ammAlice.withdrawAll(carol, XRP(0));
            BEAST_EXPECT(accountBalance(env, carol) == std::to_string(29999949999 - (5 * baseFee)));
            BEAST_EXPECT(ammAlice.expectBalances(
                XRPAmount{10'000'000'000} - priceXRP, USD(10'000), IOUAmount{5'000'000}));
            BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
            BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
        });

        // Offer crossing with two AMM LPTokens.
        testAMM([&](AMM& ammAlice, Env& env) {
            ammAlice.deposit(carol, 1'000'000);
            fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
            AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
            ammAlice1.deposit(carol, 1'000'000);
            auto const token1 = ammAlice.lptIssue();
            auto const token2 = ammAlice1.lptIssue();
            env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}), txflags(tfPassive));
            env.close();
            BEAST_EXPECT(expectOffers(env, alice, 1));
            env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
            env.close();
            BEAST_EXPECT(
                expectHolding(env, alice, STAmount{token1, 10'000'100}) &&
                expectHolding(env, alice, STAmount{token2, 9'999'900}));
            BEAST_EXPECT(
                expectHolding(env, carol, STAmount{token2, 1'000'100}) &&
                expectHolding(env, carol, STAmount{token1, 999'900}));
            BEAST_EXPECT(expectOffers(env, alice, 0) && expectOffers(env, carol, 0));
        });

        // LPs pay LPTokens directly. Must trust set because the trust line
        // is checked for the limit, which is 0 in the AMM auto-created
        // trust line.
        testAMM([&](AMM& ammAlice, Env& env) {
            auto const token1 = ammAlice.lptIssue();
            env.trust(STAmount{token1, 2'000'000}, carol);
            env.close();
            ammAlice.deposit(carol, 1'000'000);
            BEAST_EXPECT(
                ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
                ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 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{9'999'900, 0}) &&
                // Carol initial token1 1,000,000 + 100
                ammAlice.expectLPTokens(carol, IOUAmount{1'000'100, 0}));

            env.trust(STAmount{token1, 20'000'000}, alice);
            env.close();
            env(pay(carol, alice, STAmount{token1, 100}));
            env.close();
            // Back to the original balance
            BEAST_EXPECT(
                ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
                ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
        });
    }

    void
    testAmendment()
    {
        testcase("Amendment");
        FeatureBitset const all{testable_amendments()};
        FeatureBitset const noAMM{all - featureAMM};
        FeatureBitset const noNumber{all - fixUniversalNumber};
        FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber};
        using namespace jtx;

        for (auto const& feature : {noAMM, noNumber, noAMMAndNumber})
        {
            Env env{*this, feature};
            fund(env, gw, {alice}, {USD(1'000)}, Fund::All);
            AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED));

            env(amm.bid({.bidMax = 1000}), ter(temMALFORMED));
            env(amm.bid({}), ter(temDISABLED));
            amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)});
            amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)});
            amm.withdraw(WithdrawArg{.err = ter(temDISABLED)});
            amm.deposit(DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)});
            amm.ammDelete(alice, ter(temDISABLED));
        }
    }

    void
    testFlags()
    {
        testcase("Flags");
        using namespace jtx;

        testAMM([&](AMM& ammAlice, Env& env) {
            auto const info = env.rpc(
                "json",
                "account_info",
                std::string("{\"account\": \"" + to_string(ammAlice.ammAccount()) + "\"}"));
            auto const flags = info[jss::result][jss::account_data][jss::Flags].asUInt();
            BEAST_EXPECT(flags == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
        });
    }

    void
    testRippling()
    {
        testcase("Rippling");
        using namespace jtx;

        // Rippling via AMM fails because AMM trust line has 0 limit.
        // Set up two issuers, A and B. Have each issue a token called TST.
        // Have another account C hold TST from both issuers,
        //   and create an AMM for this pair.
        // Have a fourth account, D, create a trust line to the AMM for TST.
        // Send a payment delivering TST.AMM from C to D, using SendMax in
        //   TST.A (or B) and a path through the AMM account. By normal
        //   rippling rules, this would have caused the AMM's balances
        //   to shift at a 1:1 rate with no fee applied has it not been
        //   for 0 limit.
        {
            Env env(*this);
            auto const A = Account("A");
            auto const B = Account("B");
            auto const TSTA = A["TST"];
            auto const TSTB = B["TST"];
            auto const C = Account("C");
            auto const D = Account("D");

            env.fund(XRP(10'000), A);
            env.fund(XRP(10'000), B);
            env.fund(XRP(10'000), C);
            env.fund(XRP(10'000), D);

            env.trust(TSTA(10'000), C);
            env.trust(TSTB(10'000), C);
            env(pay(A, C, TSTA(10'000)));
            env(pay(B, C, TSTB(10'000)));
            AMM amm(env, C, TSTA(5'000), TSTB(5'000));
            auto const ammIss = Issue(TSTA.currency, amm.ammAccount());

            // Can TrustSet only for AMM LP tokens
            env(trust(D, STAmount{ammIss, 10'000}), ter(tecNO_PERMISSION));
            env.close();

            // The payment would fail because of above, but check just in case
            env(pay(C, D, STAmount{ammIss, 10}),
                sendmax(TSTA(100)),
                path(amm.ammAccount()),
                txflags(tfPartialPayment | tfNoRippleDirect),
                ter(tecPATH_DRY));
        }
    }

    void
    testAMMAndCLOB(FeatureBitset features)
    {
        testcase("AMMAndCLOB, offer quality change");
        using namespace jtx;
        auto const gw = Account("gw");
        auto const TST = gw["TST"];
        auto const LP1 = Account("LP1");
        auto const LP2 = Account("LP2");

        auto prep = [&](auto const& offerCb, auto const& expectCb) {
            Env env(*this, features);
            env.fund(XRP(30'000'000'000), gw);
            env(offer(gw, XRP(11'500'000'000), TST(1'000'000'000)));

            env.fund(XRP(10'000), LP1);
            env.fund(XRP(10'000), LP2);
            env(offer(LP1, TST(25), XRPAmount(287'500'000)));

            // Either AMM or CLOB offer
            offerCb(env);

            env(offer(LP2, TST(25), XRPAmount(287'500'000)));

            expectCb(env);
        };

        // If we replace AMM with an equivalent CLOB offer, which AMM generates
        // when it is consumed, then the result must be equivalent, too.
        std::string lp2TSTBalance;
        std::string lp2TakerGets;
        std::string lp2TakerPays;
        // Execute with AMM first
        prep(
            [&](Env& env) { AMM amm(env, LP1, TST(25), XRP(250)); },
            [&](Env& env) {
                lp2TSTBalance = getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString();
                auto const offer = getAccountOffers(env, LP2)["offers"][0u];
                lp2TakerGets = offer["taker_gets"].asString();
                lp2TakerPays = offer["taker_pays"]["value"].asString();
            });
        // Execute with CLOB offer
        prep(
            [&](Env& env) {
                if (!features[fixAMMv1_1])
                {
                    env(offer(
                            LP1,
                            XRPAmount{18'095'133},
                            STAmount{TST, UINT64_C(1'68737984885388), -14}),
                        txflags(tfPassive));
                }
                else
                {
                    env(offer(
                            LP1,
                            XRPAmount{18'095'132},
                            STAmount{TST, UINT64_C(1'68737976189735), -14}),
                        txflags(tfPassive));
                }
            },
            [&](Env& env) {
                BEAST_EXPECT(
                    lp2TSTBalance ==
                    getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString());
                auto const offer = getAccountOffers(env, LP2)["offers"][0u];
                BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
                BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString());
            });
    }

    void
    testTradingFee(FeatureBitset features)
    {
        testcase("Trading Fee");
        using namespace jtx;

        // Single Deposit, 1% fee
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // No fee
                ammAlice.deposit(carol, USD(3'000));
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
                ammAlice.withdrawAll(carol, USD(3'000));
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
                BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
                // Set fee to 1%
                ammAlice.vote(alice, 1'000);
                BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
                // Carol gets fewer LPToken ~994, because of the single deposit
                // fee
                ammAlice.deposit(carol, USD(3'000));
                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{994'981155689671, -12}));
                BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
                // Set fee to 0
                ammAlice.vote(alice, 0);
                ammAlice.withdrawAll(carol, USD(0));
                // Carol gets back less than the original deposit
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'96220068281), -11}));
            },
            {{USD(1'000), EUR(1'000)}},
            0,
            std::nullopt,
            {features});

        // Single deposit with EP not exceeding specified:
        // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                auto const balance = env.balance(carol, USD);
                auto tokensFee =
                    ammAlice.deposit(carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
                auto const deposit = balance - env.balance(carol, USD);
                ammAlice.withdrawAll(carol, USD(0));
                ammAlice.vote(alice, 0);
                BEAST_EXPECT(ammAlice.expectTradingFee(0));
                auto const tokensNoFee = ammAlice.deposit(carol, deposit);
                // carol pays ~2008 LPTokens in fees or ~0.5% of the no-fee
                // LPTokens
                BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7));
                BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8));
            },
            std::nullopt,
            1'000,
            std::nullopt,
            {features});

        // Single deposit with EP not exceeding specified:
        // 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                auto const balance = env.balance(carol, USD);
                auto const tokensFee =
                    ammAlice.deposit(carol, USD(200), std::nullopt, STAmount{USD, 2020, -6});
                auto const deposit = balance - env.balance(carol, USD);
                ammAlice.withdrawAll(carol, USD(0));
                ammAlice.vote(alice, 0);
                BEAST_EXPECT(ammAlice.expectTradingFee(0));
                auto const tokensNoFee = ammAlice.deposit(carol, deposit);
                // carol pays ~475 LPTokens in fees or ~0.5% of the no-fee
                // LPTokens
                BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8));
                BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8));
            },
            std::nullopt,
            1'000,
            std::nullopt,
            {features});

        // Single Withdrawal, 1% fee
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // No fee
                ammAlice.deposit(carol, USD(3'000));

                BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
                BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
                // Set fee to 1%
                ammAlice.vote(alice, 1'000);
                BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
                // Single withdrawal. Carol gets ~5USD less than deposited.
                ammAlice.withdrawAll(carol, USD(0));
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'97487437186), -11}));
            },
            {{USD(1'000), EUR(1'000)}},
            0,
            std::nullopt,
            {features});

        // Withdraw with EPrice limit, 1% fee.
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.deposit(carol, 1'000'000);
                auto const tokensFee =
                    ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0});
                // carol withdraws ~1,443.44USD
                auto const balanceAfterWithdraw = [&]() {
                    if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                    {
                        return STAmount(USD, UINT64_C(30'443'43891402715), -11);
                    }
                    if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                    {
                        return STAmount(USD, UINT64_C(30'443'43891402714), -11);
                    }

                    return STAmount(USD, UINT64_C(30'443'43891402713), -11);
                }();
                BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw);
                // Set to original pool size
                auto const deposit = balanceAfterWithdraw - USD(29'000);
                ammAlice.deposit(carol, deposit);
                // fee 0%
                ammAlice.vote(alice, 0);
                BEAST_EXPECT(ammAlice.expectTradingFee(0));
                auto const tokensNoFee = ammAlice.withdraw(carol, deposit);
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(30'443'43891402717), -11));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(30'443'43891402716), -11));
                }
                else
                {
                    BEAST_EXPECT(
                        env.balance(carol, USD) ==
                        STAmount(USD, UINT64_C(30'443'43891402713), -11));
                }
                // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee
                // LPTokens
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779913, -8));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8));
                }
                else
                {
                    BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8));
                }
                BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
            },
            std::nullopt,
            1'000,
            std::nullopt,
            {features});

        // Payment, 1% fee
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, XRP(1'000), {USD(1'000), EUR(1'000)}, Fund::Acct);
                // Alice contributed 1010EUR and 1000USD to the pool
                BEAST_EXPECT(expectHolding(env, alice, EUR(28'990)));
                BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
                BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
                // Carol pays to Alice with no fee
                env(pay(carol, alice, EUR(10)),
                    path(~EUR),
                    sendmax(USD(10)),
                    txflags(tfNoRippleDirect));
                env.close();
                // Alice has 10EUR more and Carol has 10USD less
                BEAST_EXPECT(expectHolding(env, alice, EUR(29'000)));
                BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
                BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));

                // Set fee to 1%
                ammAlice.vote(alice, 1'000);
                BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
                // Bob pays to Carol with 1% fee
                env(pay(bob, carol, USD(10)),
                    path(~USD),
                    sendmax(EUR(15)),
                    txflags(tfNoRippleDirect));
                env.close();
                // Bob sends 10.1~EUR to pay 10USD
                BEAST_EXPECT(
                    expectHolding(env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13}));
                // Carol got 10USD
                BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
                BEAST_EXPECT(ammAlice.expectBalances(
                    USD(1'000),
                    STAmount{EUR, UINT64_C(1'010'10101010101), -11},
                    ammAlice.tokens()));
            },
            {{USD(1'000), EUR(1'010)}},
            0,
            std::nullopt,
            {features});

        // Offer crossing, 0.5% fee
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                // No fee
                env(offer(carol, EUR(10), USD(10)));
                env.close();
                BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));
                BEAST_EXPECT(expectHolding(env, carol, EUR(30'010)));
                // Change pool composition back
                env(offer(carol, USD(10), EUR(10)));
                env.close();
                // Set fee to 0.5%
                ammAlice.vote(alice, 500);
                BEAST_EXPECT(ammAlice.expectTradingFee(500));
                env(offer(carol, EUR(10), USD(10)));
                env.close();
                // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes
                // to the pool
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{USD, UINT64_C(29'995'02512562814), -11}));
                BEAST_EXPECT(
                    expectHolding(env, carol, STAmount{EUR, UINT64_C(30'004'97487437186), -11}));
                BEAST_EXPECT(expectOffers(
                    env,
                    carol,
                    1,
                    {{Amounts{
                        STAmount{EUR, UINT64_C(5'025125628140703), -15},
                        STAmount{USD, UINT64_C(5'025125628140703), -15}}}}));
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        STAmount{USD, UINT64_C(1'004'974874371859), -12},
                        STAmount{EUR, UINT64_C(1'005'025125628141), -12},
                        ammAlice.tokens()));
                }
                else
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        STAmount{USD, UINT64_C(1'004'97487437186), -11},
                        STAmount{EUR, UINT64_C(1'005'025125628141), -12},
                        ammAlice.tokens()));
                }
            },
            {{USD(1'000), EUR(1'010)}},
            0,
            std::nullopt,
            {features});

        // Payment with AMM and CLOB offer, 0 fee
        // AMM liquidity is consumed first up to CLOB offer quality
        // CLOB offer is fully consumed next
        // Remaining amount is consumed via AMM liquidity
        {
            Env env(*this, features);
            Account const ed("ed");
            fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
            env(offer(carol, EUR(5), USD(5)));
            AMM ammAlice(env, alice, USD(1'005), EUR(1'000));
            env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
            BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
                BEAST_EXPECT(ammAlice.expectBalances(USD(1'000), EUR(1'005), ammAlice.tokens()));
            }
            else
            {
                BEAST_EXPECT(
                    expectHolding(env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12)));
                BEAST_EXPECT(ammAlice.expectBalances(
                    USD(1'000),
                    STAmount(EUR, UINT64_C(1005'000000000001), -12),
                    ammAlice.tokens()));
            }
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }

        // Payment with AMM and CLOB offer. Same as above but with 0.25%
        // fee.
        {
            Env env(*this, features);
            Account const ed("ed");
            fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
            env(offer(carol, EUR(5), USD(5)));
            // Set 0.25% fee
            AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 250);
            env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
            BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(
                    expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007618), -12}));
                BEAST_EXPECT(ammAlice.expectBalances(
                    USD(1'000),
                    STAmount{EUR, UINT64_C(1'005'012546992382), -12},
                    ammAlice.tokens()));
            }
            else
            {
                BEAST_EXPECT(
                    expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007628), -12}));
                BEAST_EXPECT(ammAlice.expectBalances(
                    USD(1'000),
                    STAmount{EUR, UINT64_C(1'005'012546992372), -12},
                    ammAlice.tokens()));
            }
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }

        // Payment with AMM and CLOB offer. AMM has a better
        // spot price quality, but 1% fee offsets that. As the result
        // the entire trade is executed via LOB.
        {
            Env env(*this, features);
            Account const ed("ed");
            fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
            env(offer(carol, EUR(10), USD(10)));
            // Set 1% fee
            AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
            env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
            BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
            BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
            BEAST_EXPECT(ammAlice.expectBalances(USD(1'005), EUR(1'000), ammAlice.tokens()));
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }

        // Payment with AMM and CLOB offer. AMM has a better
        // spot price quality, but 1% fee offsets that.
        // The CLOB offer is consumed first and the remaining
        // amount is consumed via AMM liquidity.
        {
            Env env(*this, features);
            Account const ed("ed");
            fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
            env(offer(carol, EUR(9), USD(9)));
            // Set 1% fee
            AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
            env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
            BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
            BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12}));
            BEAST_EXPECT(ammAlice.expectBalances(
                USD(1'004), STAmount{EUR, UINT64_C(1'001'006076703288), -12}, ammAlice.tokens()));
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }
    }

    void
    testAdjustedTokens(FeatureBitset features)
    {
        testcase("Adjusted Deposit/Withdraw Tokens");

        using namespace jtx;

        // Deposit/Withdraw in USD
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                Account const bob("bob");
                Account const ed("ed");
                Account const paul("paul");
                Account const dan("dan");
                Account const chris("chris");
                Account const simon("simon");
                Account const ben("ben");
                Account const natalie("natalie");
                fund(
                    env,
                    gw,
                    {bob, ed, paul, dan, chris, simon, ben, natalie},
                    {USD(1'500'000)},
                    Fund::Acct);
                for (int i = 0; i < 10; ++i)
                {
                    ammAlice.deposit(ben, STAmount{USD, 1, -10});
                    ammAlice.withdrawAll(ben, USD(0));
                    ammAlice.deposit(simon, USD(0.1));
                    ammAlice.withdrawAll(simon, USD(0));
                    ammAlice.deposit(chris, USD(1));
                    ammAlice.withdrawAll(chris, USD(0));
                    ammAlice.deposit(dan, USD(10));
                    ammAlice.withdrawAll(dan, USD(0));
                    ammAlice.deposit(bob, USD(100));
                    ammAlice.withdrawAll(bob, USD(0));
                    ammAlice.deposit(carol, USD(1'000));
                    ammAlice.withdrawAll(carol, USD(0));
                    ammAlice.deposit(ed, USD(10'000));
                    ammAlice.withdrawAll(ed, USD(0));
                    ammAlice.deposit(paul, USD(100'000));
                    ammAlice.withdrawAll(paul, USD(0));
                    ammAlice.deposit(natalie, USD(1'000'000));
                    ammAlice.withdrawAll(natalie, USD(0));
                }
                // Due to round off some accounts have a tiny gain, while
                // other have a tiny loss. The last account to withdraw
                // gets everything in the pool.
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'000),
                        STAmount{USD, UINT64_C(10'000'0000000013), -10},
                        IOUAmount{10'000'000}));
                }
                else if (features[fixAMMv1_3])
                {
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRP(10'000),
                        STAmount{USD, UINT64_C(10'000'0000000003), -10},
                        IOUAmount{10'000'000}));
                }
                else
                {
                    BEAST_EXPECT(
                        ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
                }
                BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000)));
                BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000)));
                BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000)));
                BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000)));
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(expectHolding(
                        env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11}));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
                }
                else
                {
                    BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
                }
                BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000)));
                BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000)));
                if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(expectHolding(
                        env, natalie, STAmount{USD, UINT64_C(1'500'000'000000002), -9}));
                }
                else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
                {
                    BEAST_EXPECT(expectHolding(
                        env, natalie, STAmount{USD, UINT64_C(1'500'000'000000005), -9}));
                }
                else
                {
                    BEAST_EXPECT(expectHolding(env, natalie, USD(1'500'000)));
                }
                ammAlice.withdrawAll(alice);
                BEAST_EXPECT(!ammAlice.ammExists());
                if (!features[fixAMMv1_1])
                {
                    BEAST_EXPECT(
                        expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10}));
                }
                else if (features[fixAMMv1_3])
                {
                    BEAST_EXPECT(
                        expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000003), -10}));
                }
                else
                {
                    BEAST_EXPECT(expectHolding(env, alice, USD(30'000)));
                }
                // alice XRP balance is 30,000 initial - 50 AMMCreate fee -
                // 10drops fee
                BEAST_EXPECT(
                    accountBalance(env, alice) ==
                    std::to_string(29950000000 - env.current()->fees().base.drops()));
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});

        // Same as above but deposit/withdraw in XRP
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                Account const bob("bob");
                Account const ed("ed");
                Account const paul("paul");
                Account const dan("dan");
                Account const chris("chris");
                Account const simon("simon");
                Account const ben("ben");
                Account const natalie("natalie");
                fund(
                    env,
                    gw,
                    {bob, ed, paul, dan, chris, simon, ben, natalie},
                    XRP(2'000'000),
                    {},
                    Fund::Acct);
                for (int i = 0; i < 10; ++i)
                {
                    ammAlice.deposit(ben, XRPAmount{1});
                    ammAlice.withdrawAll(ben, XRP(0));
                    ammAlice.deposit(simon, XRPAmount(1'000));
                    ammAlice.withdrawAll(simon, XRP(0));
                    ammAlice.deposit(chris, XRP(1));
                    ammAlice.withdrawAll(chris, XRP(0));
                    ammAlice.deposit(dan, XRP(10));
                    ammAlice.withdrawAll(dan, XRP(0));
                    ammAlice.deposit(bob, XRP(100));
                    ammAlice.withdrawAll(bob, XRP(0));
                    ammAlice.deposit(carol, XRP(1'000));
                    ammAlice.withdrawAll(carol, XRP(0));
                    ammAlice.deposit(ed, XRP(10'000));
                    ammAlice.withdrawAll(ed, XRP(0));
                    ammAlice.deposit(paul, XRP(100'000));
                    ammAlice.withdrawAll(paul, XRP(0));
                    ammAlice.deposit(natalie, XRP(1'000'000));
                    ammAlice.withdrawAll(natalie, XRP(0));
                }
                auto const baseFee = env.current()->fees().base.drops();
                if (!features[fixAMMv1_3])
                {
                    // No round off with XRP in this test
                    BEAST_EXPECT(
                        ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
                    ammAlice.withdrawAll(alice);
                    BEAST_EXPECT(!ammAlice.ammExists());
                    // 20,000 initial - (deposit+withdraw) * 10
                    auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText();
                    BEAST_EXPECT(accountBalance(env, ben) == xrpBalance);
                    BEAST_EXPECT(accountBalance(env, simon) == xrpBalance);
                    BEAST_EXPECT(accountBalance(env, chris) == xrpBalance);
                    BEAST_EXPECT(accountBalance(env, dan) == xrpBalance);

                    // 30,000 initial - (deposit+withdraw) * 10
                    BEAST_EXPECT(
                        accountBalance(env, carol) ==
                        std::to_string(30'000'000'000 - (20 * baseFee)));
                    BEAST_EXPECT(accountBalance(env, ed) == xrpBalance);
                    BEAST_EXPECT(accountBalance(env, paul) == xrpBalance);
                    BEAST_EXPECT(accountBalance(env, natalie) == xrpBalance);
                    // 30,000 initial - 50 AMMCreate fee - 10drops withdraw fee
                    BEAST_EXPECT(
                        accountBalance(env, alice) == std::to_string(29'950'000'000 - baseFee));
                }
                else
                {
                    // post-amendment the rounding takes place to ensure
                    // AMM invariant
                    BEAST_EXPECT(ammAlice.expectBalances(
                        XRPAmount(10'000'000'080), USD(10'000), IOUAmount{10'000'000}));
                    ammAlice.withdrawAll(alice);
                    BEAST_EXPECT(!ammAlice.ammExists());
                    auto const xrpBalance = XRP(2'000'000) - txfee(env, 20) - drops(10);
                    auto const xrpBalanceText = xrpBalance.getText();
                    BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText);
                    BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText);
                    BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText);
                    BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText);
                    BEAST_EXPECT(
                        accountBalance(env, carol) ==
                        std::to_string(30'000'000'000 - (20 * baseFee) - 10));
                    BEAST_EXPECT(accountBalance(env, ed) == (xrpBalance + drops(2)).getText());
                    BEAST_EXPECT(accountBalance(env, paul) == (xrpBalance + drops(3)).getText());
                    BEAST_EXPECT(accountBalance(env, natalie) == (xrpBalance + drops(5)).getText());
                    BEAST_EXPECT(
                        accountBalance(env, alice) ==
                        std::to_string(29'950'000'000 - baseFee + 80));
                }
            },
            std::nullopt,
            0,
            std::nullopt,
            {features});
    }

    void
    testAutoDelete()
    {
        testcase("Auto Delete");

        using namespace jtx;
        FeatureBitset const all{testable_amendments()};

        {
            Env env(
                *this,
                envconfig([](std::unique_ptr<Config> cfg) {
                    cfg->FEES.reference_fee = XRPAmount(1);
                    return cfg;
                }),
                all);
            fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
            AMM amm(env, gw, XRP(10'000), USD(10'000));
            for (auto i = 0; i < maxDeletableAMMTrustLines + 10; ++i)
            {
                Account const a{std::to_string(i)};
                env.fund(XRP(1'000), a);
                env(trust(a, STAmount{amm.lptIssue(), 10'000}));
                env.close();
            }
            // The trustlines are partially deleted,
            // AMM is set to an empty state.
            amm.withdrawAll(gw);
            BEAST_EXPECT(amm.ammExists());

            // Bid,Vote,Deposit,Withdraw,TrustSet failing with
            // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
            env(amm.bid({
                    .account = alice,
                    .bidMin = 1000,
                }),
                ter(tecAMM_EMPTY));
            amm.vote(
                std::nullopt, 100, std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
            amm.withdraw(alice, 100, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
            amm.deposit(
                alice, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
            env(trust(alice, STAmount{amm.lptIssue(), 10'000}), ter(tecAMM_EMPTY));

            // Can deposit with tfTwoAssetIfEmpty option
            amm.deposit(
                alice,
                std::nullopt,
                XRP(10'000),
                USD(10'000),
                std::nullopt,
                tfTwoAssetIfEmpty,
                std::nullopt,
                std::nullopt,
                1'000);
            BEAST_EXPECT(amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens()));
            BEAST_EXPECT(amm.expectTradingFee(1'000));
            BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));

            // Withdrawing all tokens deletes AMM since the number
            // of remaining trustlines is less than max
            amm.withdrawAll(alice);
            BEAST_EXPECT(!amm.ammExists());
            BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
        }

        {
            Env env(
                *this,
                envconfig([](std::unique_ptr<Config> cfg) {
                    cfg->FEES.reference_fee = XRPAmount(1);
                    return cfg;
                }),
                all);
            fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
            AMM amm(env, gw, XRP(10'000), USD(10'000));
            for (auto i = 0; i < (maxDeletableAMMTrustLines * 2) + 10; ++i)
            {
                Account const a{std::to_string(i)};
                env.fund(XRP(1'000), a);
                env(trust(a, STAmount{amm.lptIssue(), 10'000}));
                env.close();
            }
            // The trustlines are partially deleted.
            amm.withdrawAll(gw);
            BEAST_EXPECT(amm.ammExists());

            // AMMDelete has to be called twice to delete AMM.
            amm.ammDelete(alice, ter(tecINCOMPLETE));
            BEAST_EXPECT(amm.ammExists());
            // Deletes remaining trustlines and deletes AMM.
            amm.ammDelete(alice);
            BEAST_EXPECT(!amm.ammExists());
            BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));

            // Try redundant delete
            amm.ammDelete(alice, ter(terNO_AMM));
        }
    }

    void
    testClawback()
    {
        testcase("Clawback");
        using namespace jtx;
        Env env(*this);
        env.fund(XRP(2'000), gw);
        env.fund(XRP(2'000), alice);
        AMM amm(env, gw, XRP(1'000), USD(1'000));
        env(fset(gw, asfAllowTrustLineClawback), ter(tecOWNERS));
    }

    void
    testAMMID()
    {
        testcase("AMMID");
        using namespace jtx;
        testAMM([&](AMM& amm, Env& env) {
            amm.setClose(false);
            auto const info = env.rpc(
                "json",
                "account_info",
                std::string("{\"account\": \"" + to_string(amm.ammAccount()) + "\"}"));
            try
            {
                BEAST_EXPECT(
                    info[jss::result][jss::account_data][jss::AMMID].asString() ==
                    to_string(amm.ammID()));
            }
            catch (...)
            {
                fail();
            }
            amm.deposit(carol, 1'000);
            auto affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName];
            try
            {
                bool found = false;
                for (auto const& node : affected)
                {
                    if (node.isMember(sfModifiedNode.fieldName) &&
                        node[sfModifiedNode.fieldName][sfLedgerEntryType.fieldName].asString() ==
                            "AccountRoot" &&
                        node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::Account]
                                .asString() == to_string(amm.ammAccount()))
                    {
                        found = node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::AMMID]
                                    .asString() == to_string(amm.ammID());
                        break;
                    }
                }
                BEAST_EXPECT(found);
            }
            catch (...)
            {
                fail();
            }
        });
    }

    void
    testSelection(FeatureBitset features)
    {
        testcase("Offer/Strand Selection");
        using namespace jtx;
        Account const ed("ed");
        Account const gw1("gw1");
        auto const ETH = gw1["ETH"];
        auto const CAN = gw1["CAN"];

        // These tests are expected to fail if the OwnerPaysFee feature
        // is ever supported. Updates will need to be made to AMM handling
        // in the payment engine, and these tests will need to be updated.

        auto prep = [&](Env& env, auto gwRate, auto gw1Rate) {
            fund(env, gw, {alice, carol, bob, ed}, XRP(2'000), {USD(2'000)});
            env.fund(XRP(2'000), gw1);
            fund(env, gw1, {alice, carol, bob, ed}, {ETH(2'000), CAN(2'000)}, Fund::IOUOnly);
            env(rate(gw, gwRate));
            env(rate(gw1, gw1Rate));
            env.close();
        };

        for (auto const& rates : {std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)})
        {
            // Offer Selection

            // Cross-currency payment: AMM has the same spot price quality
            // as CLOB's offer and can't generate a better quality offer.
            // The transfer fee in this case doesn't change the CLOB quality
            // because trIn is ignored on adjustment and trOut on payment is
            // also ignored because ownerPaysTransferFee is false in this
            // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
            // verify that the quality is better in the first case, and CLOB
            // is selected in the second case.
            {
                std::array<Quality, 3> q{};
                for (auto i = 0; i < 3; ++i)
                {
                    Env env(*this, features);
                    prep(env, rates.first, rates.second);
                    std::optional<AMM> amm;
                    if (i == 0 || i == 2)
                    {
                        env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
                        env.close();
                    }
                    if (i > 0)
                        amm.emplace(env, ed, USD(1'000), ETH(1'000));
                    env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500)));
                    env.close();
                    // CLOB and AMM, AMM is not selected
                    if (i == 2)
                    {
                        BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
                    }
                    BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
                    q[i] = Quality(
                        Amounts{
                            ETH(2'000) - env.balance(carol, ETH),
                            env.balance(bob, USD) - USD(2'000)});
                }
                // CLOB is better quality than AMM
                BEAST_EXPECT(q[0] > q[1]);
                // AMM is not selected with CLOB
                BEAST_EXPECT(q[0] == q[2]);
            }
            // Offer crossing: AMM has the same spot price quality
            // as CLOB's offer and can't generate a better quality offer.
            // The transfer fee in this case doesn't change the CLOB quality
            // because the quality adjustment is ignored for the offer
            // crossing.
            for (auto i = 0; i < 3; ++i)
            {
                Env env(*this, features);
                prep(env, rates.first, rates.second);
                std::optional<AMM> amm;
                if (i == 0 || i == 2)
                {
                    env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
                    env.close();
                }
                if (i > 0)
                    amm.emplace(env, ed, USD(1'000), ETH(1'000));
                env(offer(alice, USD(400), ETH(400)));
                env.close();
                // AMM is not selected
                if (i > 0)
                {
                    BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
                }
                if (i == 0 || i == 2)
                {
                    // Fully crosses
                    BEAST_EXPECT(expectOffers(env, alice, 0));
                }
                // Fails to cross because AMM is not selected
                else
                {
                    BEAST_EXPECT(expectOffers(env, alice, 1, {Amounts{USD(400), ETH(400)}}));
                }
                BEAST_EXPECT(expectOffers(env, ed, 0));
            }

            // Show that the CLOB quality reduction
            // results in AMM offer selection.

            // Same as the payment but reduced offer quality
            {
                std::array<Quality, 3> q{};
                for (auto i = 0; i < 3; ++i)
                {
                    Env env(*this, features);
                    prep(env, rates.first, rates.second);
                    std::optional<AMM> amm;
                    if (i == 0 || i == 2)
                    {
                        env(offer(ed, ETH(400), USD(300)), txflags(tfPassive));
                        env.close();
                    }
                    if (i > 0)
                        amm.emplace(env, ed, USD(1'000), ETH(1'000));
                    env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500)));
                    env.close();
                    // AMM and CLOB are selected
                    if (i > 0)
                    {
                        BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
                    }
                    if (i == 2 && !features[fixAMMv1_1])
                    {
                        if (rates.first == 1.5)
                        {
                            if (!features[fixAMMv1_1])
                            {
                                BEAST_EXPECT(expectOffers(
                                    env,
                                    ed,
                                    1,
                                    {{Amounts{
                                        STAmount{ETH, UINT64_C(378'6327949540823), -13},
                                        STAmount{USD, UINT64_C(283'9745962155617), -13}}}}));
                            }
                            else
                            {
                                BEAST_EXPECT(expectOffers(
                                    env,
                                    ed,
                                    1,
                                    {{Amounts{
                                        STAmount{ETH, UINT64_C(378'6327949540813), -13},
                                        STAmount{USD, UINT64_C(283'974596215561), -12}}}}));
                            }
                        }
                        else
                        {
                            if (!features[fixAMMv1_1])
                            {
                                BEAST_EXPECT(expectOffers(
                                    env,
                                    ed,
                                    1,
                                    {{Amounts{
                                        STAmount{ETH, UINT64_C(325'299461620749), -12},
                                        STAmount{USD, UINT64_C(243'9745962155617), -13}}}}));
                            }
                            else
                            {
                                BEAST_EXPECT(expectOffers(
                                    env,
                                    ed,
                                    1,
                                    {{Amounts{
                                        STAmount{ETH, UINT64_C(325'299461620748), -12},
                                        STAmount{USD, UINT64_C(243'974596215561), -12}}}}));
                            }
                        }
                    }
                    else if (i == 2)
                    {
                        if (rates.first == 1.5)
                        {
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                1,
                                {{Amounts{
                                    STAmount{ETH, UINT64_C(378'6327949540812), -13},
                                    STAmount{USD, UINT64_C(283'9745962155609), -13}}}}));
                        }
                        else
                        {
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                1,
                                {{Amounts{
                                    STAmount{ETH, UINT64_C(325'2994616207479), -13},
                                    STAmount{USD, UINT64_C(243'9745962155609), -13}}}}));
                        }
                    }
                    BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
                    q[i] = Quality(
                        Amounts{
                            ETH(2'000) - env.balance(carol, ETH),
                            env.balance(bob, USD) - USD(2'000)});
                }
                // AMM is better quality
                BEAST_EXPECT(q[1] > q[0]);
                // AMM and CLOB produce better quality
                BEAST_EXPECT(q[2] > q[1]);
            }

            // Same as the offer-crossing but reduced offer quality
            for (auto i = 0; i < 3; ++i)
            {
                Env env(*this, features);
                prep(env, rates.first, rates.second);
                std::optional<AMM> amm;
                if (i == 0 || i == 2)
                {
                    env(offer(ed, ETH(400), USD(250)), txflags(tfPassive));
                    env.close();
                }
                if (i > 0)
                    amm.emplace(env, ed, USD(1'000), ETH(1'000));
                env(offer(alice, USD(250), ETH(400)));
                env.close();
                // AMM is selected in both cases
                if (i > 0)
                {
                    BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
                }
                // Partially crosses, AMM is selected, CLOB fails
                // limitQuality
                if (i == 2)
                {
                    if (rates.first == 1.5)
                    {
                        if (!features[fixAMMv1_1])
                        {
                            BEAST_EXPECT(expectOffers(env, ed, 1, {{Amounts{ETH(400), USD(250)}}}));
                            BEAST_EXPECT(expectOffers(
                                env,
                                alice,
                                1,
                                {{Amounts{
                                    STAmount{USD, UINT64_C(40'5694150420947), -13},
                                    STAmount{ETH, UINT64_C(64'91106406735152), -14},
                                }}}));
                        }
                        else
                        {
                            // Ed offer is partially crossed.
                            // The updated rounding makes limitQuality
                            // work if both amendments are enabled
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                1,
                                {{Amounts{
                                    STAmount{ETH, UINT64_C(335'0889359326475), -13},
                                    STAmount{USD, UINT64_C(209'4305849579047), -13},
                                }}}));
                            BEAST_EXPECT(expectOffers(env, alice, 0));
                        }
                    }
                    else
                    {
                        if (!features[fixAMMv1_1])
                        {
                            // Ed offer is partially crossed.
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                1,
                                {{Amounts{
                                    STAmount{ETH, UINT64_C(335'0889359326485), -13},
                                    STAmount{USD, UINT64_C(209'4305849579053), -13},
                                }}}));
                            BEAST_EXPECT(expectOffers(env, alice, 0));
                        }
                        else
                        {
                            // Ed offer is partially crossed.
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                1,
                                {{Amounts{
                                    STAmount{ETH, UINT64_C(335'0889359326475), -13},
                                    STAmount{USD, UINT64_C(209'4305849579047), -13},
                                }}}));
                            BEAST_EXPECT(expectOffers(env, alice, 0));
                        }
                    }
                }
            }

            // Strand selection

            // Two book steps strand quality is 1.
            // AMM strand's best quality is equal to AMM's spot price
            // quality, which is 1. Both strands (steps) are adjusted
            // for the transfer fee in qualityUpperBound. In case
            // of two strands, AMM offers have better quality and are
            // consumed first, remaining liquidity is generated by CLOB
            // offers. Liquidity from two strands is better in this case
            // than in case of one strand with two book steps. Liquidity
            // from one strand with AMM has better quality than either one
            // strand with two book steps or two strands. It may appear
            // unintuitive, but one strand with AMM is optimized and
            // generates one AMM offer, while in case of two strands,
            // multiple AMM offers are generated, which results in slightly
            // worse overall quality.
            {
                std::array<Quality, 3> q{};
                for (auto i = 0; i < 3; ++i)
                {
                    Env env(*this, features);
                    prep(env, rates.first, rates.second);
                    std::optional<AMM> amm;

                    if (i == 0 || i == 2)
                    {
                        env(offer(ed, ETH(400), CAN(400)), txflags(tfPassive));
                        env(offer(ed, CAN(400), USD(400))), txflags(tfPassive);
                        env.close();
                    }

                    if (i > 0)
                        amm.emplace(env, ed, ETH(1'000), USD(1'000));

                    env(pay(carol, bob, USD(100)), path(~USD), path(~CAN, ~USD), sendmax(ETH(600)));
                    env.close();

                    BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));

                    if (i == 2 && !features[fixAMMv1_1])
                    {
                        if (rates.first == 1.5)
                        {
                            // Liquidity is consumed from AMM strand only
                            BEAST_EXPECT(amm->expectBalances(
                                STAmount{ETH, UINT64_C(1'176'66038955758), -11},
                                USD(850),
                                amm->tokens()));
                        }
                        else
                        {
                            BEAST_EXPECT(amm->expectBalances(
                                STAmount{ETH, UINT64_C(1'179'540094339627), -12},
                                STAmount{USD, UINT64_C(847'7880529867501), -13},
                                amm->tokens()));
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                2,
                                {{Amounts{
                                      STAmount{ETH, UINT64_C(343'3179205198749), -13},
                                      STAmount{CAN, UINT64_C(343'3179205198749), -13},
                                  },
                                  Amounts{
                                      STAmount{CAN, UINT64_C(362'2119470132499), -13},
                                      STAmount{USD, UINT64_C(362'2119470132499), -13},
                                  }}}));
                        }
                    }
                    else if (i == 2)
                    {
                        if (rates.first == 1.5)
                        {
                            // Liquidity is consumed from AMM strand only
                            BEAST_EXPECT(amm->expectBalances(
                                STAmount{ETH, UINT64_C(1'176'660389557593), -12},
                                USD(850),
                                amm->tokens()));
                        }
                        else
                        {
                            BEAST_EXPECT(amm->expectBalances(
                                STAmount{ETH, UINT64_C(1'179'54009433964), -11},
                                STAmount{USD, UINT64_C(847'7880529867501), -13},
                                amm->tokens()));
                            BEAST_EXPECT(expectOffers(
                                env,
                                ed,
                                2,
                                {{Amounts{
                                      STAmount{ETH, UINT64_C(343'3179205198749), -13},
                                      STAmount{CAN, UINT64_C(343'3179205198749), -13},
                                  },
                                  Amounts{
                                      STAmount{CAN, UINT64_C(362'2119470132499), -13},
                                      STAmount{USD, UINT64_C(362'2119470132499), -13},
                                  }}}));
                        }
                    }
                    q[i] = Quality(
                        Amounts{
                            ETH(2'000) - env.balance(carol, ETH),
                            env.balance(bob, USD) - USD(2'000)});
                }
                BEAST_EXPECT(q[1] > q[0]);
                BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
            }
        }
    }

    void
    testFixDefaultInnerObj()
    {
        testcase("Fix Default Inner Object");
        using namespace jtx;
        FeatureBitset const all{testable_amendments()};

        auto test = [&](FeatureBitset features,
                        TER const& err1,
                        TER const& err2,
                        TER const& err3,
                        TER const& err4,
                        std::uint16_t tfee,
                        bool closeLedger,
                        std::optional<std::uint16_t> extra = std::nullopt) {
            Env env(*this, features);
            fund(env, gw, {alice}, XRP(1'000), {USD(10)});
            AMM amm(env, gw, XRP(10), USD(10), {.tfee = tfee, .close = closeLedger});
            amm.deposit(alice, USD(10), XRP(10));
            amm.vote(VoteArg{.account = alice, .tfee = tfee, .err = ter(err1)});
            amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(1), .err = ter(err2)});
            // with the amendment disabled and ledger not closed,
            // second vote succeeds if the first vote sets the trading fee
            // to non-zero; if the first vote sets the trading fee to >0 &&
            // <9 then the second withdraw succeeds if the second vote sets
            // the trading fee so that the discounted fee is non-zero
            amm.vote(VoteArg{.account = alice, .tfee = 20, .err = ter(err3)});
            amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(2), .err = ter(err4)});
        };

        // ledger is closed after each transaction, vote/withdraw don't fail
        // regardless whether the amendment is enabled or not
        test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
        // ledger is not closed after each transaction
        // vote/withdraw don't fail if the amendment is enabled
        test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
        // vote/withdraw fail if the amendment is not enabled
        // second vote/withdraw still fail: second vote fails because
        // the initial trading fee is 0, consequently second withdraw fails
        // because the second vote fails
        test(
            all - fixInnerObjTemplate,
            tefEXCEPTION,
            tefEXCEPTION,
            tefEXCEPTION,
            tefEXCEPTION,
            0,
            false);
        // if non-zero trading/discounted fee then vote/withdraw
        // don't fail whether the ledger is closed or not and
        // the amendment is enabled or not
        test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
        test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
        test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
        // non-zero trading fee but discounted fee is 0, vote doesn't fail
        // but withdraw fails
        test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
        // second vote sets the trading fee to non-zero, consequently
        // second withdraw doesn't fail even if the amendment is not
        // enabled and the ledger is not closed
        test(all - fixInnerObjTemplate, tesSUCCESS, tefEXCEPTION, tesSUCCESS, tesSUCCESS, 9, false);
    }

    void
    testFixChangeSpotPriceQuality(FeatureBitset features)
    {
        testcase("Fix changeSpotPriceQuality");
        using namespace jtx;

        std::string logs;

        enum class Status {
            SucceedShouldSucceedResize,  // Succeed in pre-fix because
                                         // error allowance, succeed post-fix
                                         // because of offer resizing
            FailShouldSucceed,           // Fail in pre-fix due to rounding,
                                         // succeed after fix because of XRP
                                         // side is generated first
            SucceedShouldFail,           // Succeed in pre-fix, fail after fix
                                         // due to small quality difference
            Fail,                        // Both fail because the quality can't be matched
            Succeed                      // Both succeed
        };
        using enum Status;
        auto const xrpIouAmounts10_100 = TAmounts{XRPAmount{10}, IOUAmount{100}};
        auto const iouXrpAmounts10_100 = TAmounts{IOUAmount{10}, XRPAmount{100}};
        // clang-format off
        std::vector<std::tuple<std::string, std::string, Quality, std::uint16_t, Status>> tests = {
            //Pool In              ,  Pool Out,             Quality                     , Fee,  Status
            {"0.001519763260828713", "1558701",             Quality{5414253689393440221}, 1000, FailShouldSucceed},
            {"0.01099814367603737",  "1892611",             Quality{5482264816516900274}, 1000, FailShouldSucceed},
            {"0.78",                 "796599",              Quality{5630392334958379008}, 1000, FailShouldSucceed},
            {"105439.2955578965",    "49398693",            Quality{5910869983721805038},  400, FailShouldSucceed},
            {"12408293.23445213",    "4340810521",          Quality{5911611095910090752},  997, FailShouldSucceed},
            {"1892611",              "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed},
            {"423028.8508101858",    "3392804520",          Quality{5837920340654162816},  600, FailShouldSucceed},
            {"44565388.41001027",    "73890647",            Quality{6058976634606450001}, 1000, FailShouldSucceed},
            {"66831.68494832662",    "16",                  Quality{6346111134641742975},    0, FailShouldSucceed},
            {"675.9287302203422",    "1242632304",          Quality{5625960929244093294},  300, FailShouldSucceed},
            {"7047.112186735699",    "1649845866",          Quality{5696855348026306945},  504, FailShouldSucceed},
            {"840236.4402981238",    "47419053",            Quality{5982561601648018688},  499, FailShouldSucceed},
            {"992715.618909774",     "189445631733",        Quality{5697835648288106944},  815, SucceedShouldSucceedResize},
            {"504636667521",         "185545883.9506651",   Quality{6343802275337659280},  503, SucceedShouldSucceedResize},
            {"992706.7218636649",    "189447316000",        Quality{5697835648288106944},  797, SucceedShouldSucceedResize},
            {"1.068737911388205",    "127860278877",        Quality{5268604356368739396},  293, SucceedShouldSucceedResize},
            {"17932506.56880419",    "189308.6043676173",   Quality{6206460598195440068},  311, SucceedShouldSucceedResize},
            {"1.066379294658174",    "128042251493",        Quality{5268559341368739328},  270, SucceedShouldSucceedResize},
            {"350131413924",         "1576879.110907892",   Quality{6487411636539049449},  650, Fail},
            {"422093460",            "2.731797662057464",   Quality{6702911108534394924}, 1000, Fail},
            {"76128132223",          "367172.7148422662",   Quality{6487263463413514240},  548, Fail},
            {"132701839250",         "280703770.7695443",   Quality{6273750681188885075},  562, Fail},
            {"994165.7604612011",    "189551302411",        Quality{5697835592690668727},  815, Fail},
            {"45053.33303227917",    "86612695359",         Quality{5625695218943638190},  500, Fail},
            {"199649.077043865",     "14017933007",         Quality{5766034667318524880},  324, Fail},
            {"27751824831.70903",    "78896950",            Quality{6272538159621630432},  500, Fail},
            {"225.3731275781907",    "156431793648",        Quality{5477818047604078924},  989, Fail},
            {"199649.077043865",     "14017933007",         Quality{5766036094462806309},  324, Fail},
            {"3.590272027140361",    "20677643641",         Quality{5406056147042156356},  808, Fail},
            {"1.070884664490231",    "127604712776",        Quality{5268620608623825741},  293, Fail},
            {"3272.448829820197",    "6275124076",          Quality{5625710328924117902},   81, Fail},
            {"0.009059512633902926", "7994028",             Quality{5477511954775533172}, 1000, Fail},
            {"1",                    "1.0",                 Quality{0},                    100, Fail},
            {"1.0",                  "1",                   Quality{0},                    100, Fail},
            {"10",                   "10.0",                Quality{xrpIouAmounts10_100},  100, Fail},
            {"10.0",                 "10",                  Quality{iouXrpAmounts10_100},  100, Fail},
            {"69864389131",          "287631.4543025075",   Quality{6487623473313516078},  451, Succeed},
            {"4328342973",           "12453825.99247381",   Quality{6272522264364865181},  997, Succeed},
            {"32347017",             "7003.93031579449",    Quality{6347261126087916670}, 1000, Succeed},
            {"61697206161",          "36631.4583206413",    Quality{6558965195382476659},  500, Succeed},
            {"1654524979",           "7028.659825511603",   Quality{6487551345110052981},  504, Succeed},
            {"88621.22277293179",    "5128418948",          Quality{5766347291552869205},  380, Succeed},
            {"1892611",              "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed},
            {"4542.639373338766",    "24554809",            Quality{5838994982188783710},    0, Succeed},
            {"5132932546",           "88542.99750172683",   Quality{6419203342950054537},  380, Succeed},
            {"78929964.1549083",     "1506494795",          Quality{5986890029845558688},  589, Succeed},
            {"10096561906",          "44727.72453735605",   Quality{6487455290284644551},  250, Succeed},
            {"5092.219565514988",    "8768257694",          Quality{5626349534958379008},  503, Succeed},
            {"1819778294",           "8305.084302902864",   Quality{6487429398998540860},  415, Succeed},
            {"6970462.633911943",    "57359281",            Quality{6054087899185946624},  850, Succeed},
            {"3983448845",           "2347.543644281467",   Quality{6558965195382476659},  856, Succeed},
            // This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance.
            // Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than
            // the target quality and the offer fails.
            {"771493171",            "1.243473020567508",   Quality{6707566798038544272},  100, SucceedShouldFail},
        };
        // clang-format on

        boost::regex rx("^\\d+$");
        boost::smatch match;
        // tests that succeed should have the same amounts pre-fix and post-fix
        std::vector<std::pair<STAmount, STAmount>> successAmounts;
        Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
        auto rules = env.current()->rules();
        CurrentTransactionRulesGuard rg(rules);
        NumberMantissaScaleGuard sg(MantissaRange::small);

        for (auto const& t : tests)
        {
            auto getPool = [&](std::string const& v, bool isXRP) {
                if (isXRP)
                    return amountFromString(xrpIssue(), v);
                return amountFromString(noIssue(), v);
            };
            auto const& quality = std::get<Quality>(t);
            auto const tfee = std::get<std::uint16_t>(t);
            auto const status = std::get<Status>(t);
            auto const poolInIsXRP = boost::regex_search(std::get<0>(t), match, rx);
            auto const poolOutIsXRP = boost::regex_search(std::get<1>(t), match, rx);
            assert(!(poolInIsXRP && poolOutIsXRP));
            auto const poolIn = getPool(std::get<0>(t), poolInIsXRP);
            auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP);
            try
            {
                auto const amounts = changeSpotPriceQuality(
                    Amounts{poolIn, poolOut}, quality, tfee, env.current()->rules(), env.journal);
                if (amounts)
                {
                    if (status == SucceedShouldSucceedResize)
                    {
                        if (!features[fixAMMv1_1])
                        {
                            BEAST_EXPECT(Quality{*amounts} < quality);
                        }
                        else
                        {
                            BEAST_EXPECT(Quality{*amounts} >= quality);
                        }
                    }
                    else if (status == Succeed)
                    {
                        if (!features[fixAMMv1_1])
                        {
                            BEAST_EXPECT(
                                Quality{*amounts} >= quality ||
                                withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
                        }
                        else
                        {
                            BEAST_EXPECT(Quality{*amounts} >= quality);
                        }
                    }
                    else if (status == FailShouldSucceed)
                    {
                        BEAST_EXPECT(features[fixAMMv1_1] && Quality{*amounts} >= quality);
                    }
                    else if (status == SucceedShouldFail)
                    {
                        BEAST_EXPECT(
                            !features[fixAMMv1_1] && Quality{*amounts} < quality &&
                            withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
                    }
                }
                else
                {
                    // Fails pre- and post-amendment because the quality can't
                    // be matched. Verify by generating a tiny offer, which
                    // doesn't match the quality. Exclude zero quality since
                    // no offer is generated in this case.
                    if (status == Fail && quality != Quality{0})
                    {
                        auto tinyOffer = [&]() {
                            if (isXRP(poolIn))
                            {
                                auto const takerPays = STAmount{xrpIssue(), 1};
                                return Amounts{
                                    takerPays,
                                    swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
                            }
                            if (isXRP(poolOut))
                            {
                                auto const takerGets = STAmount{xrpIssue(), 1};
                                return Amounts{
                                    swapAssetOut(Amounts{poolIn, poolOut}, takerGets, tfee),
                                    takerGets};
                            }
                            auto const takerPays =
                                toAmount<STAmount>(getIssue(poolIn), Number{1, -10} * poolIn);
                            return Amounts{
                                takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
                        }();
                        BEAST_EXPECT(Quality(tinyOffer) < quality);
                    }
                    else if (status == FailShouldSucceed)
                    {
                        BEAST_EXPECT(!features[fixAMMv1_1]);
                    }
                    else if (status == SucceedShouldFail)
                    {
                        BEAST_EXPECT(features[fixAMMv1_1]);
                    }
                }
            }
            catch (std::runtime_error const& e)
            {
                BEAST_EXPECT(!strcmp(e.what(), "changeSpotPriceQuality failed"));
                BEAST_EXPECT(!features[fixAMMv1_1] && status == FailShouldSucceed);
            }
        }

        // Test negative discriminant
        {
            // b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3
            auto const res = solveQuadraticEqSmallest(Number{1}, Number{1}, Number{1});
            BEAST_EXPECT(!res.has_value());
        }
    }

    void
    testMalformed()
    {
        using namespace jtx;

        testAMM([&](AMM& ammAlice, Env& env) {
            WithdrawArg args{
                .flags = tfSingleAsset,
                .err = ter(temMALFORMED),
            };
            ammAlice.withdraw(args);
        });

        testAMM([&](AMM& ammAlice, Env& env) {
            WithdrawArg args{
                .flags = tfOneAssetLPToken,
                .err = ter(temMALFORMED),
            };
            ammAlice.withdraw(args);
        });

        testAMM([&](AMM& ammAlice, Env& env) {
            WithdrawArg args{
                .flags = tfLimitLPToken,
                .err = ter(temMALFORMED),
            };
            ammAlice.withdraw(args);
        });

        testAMM([&](AMM& ammAlice, Env& env) {
            WithdrawArg args{
                .asset1Out = XRP(100),
                .asset2Out = XRP(100),
                .err = ter(temBAD_AMM_TOKENS),
            };
            ammAlice.withdraw(args);
        });

        testAMM([&](AMM& ammAlice, Env& env) {
            WithdrawArg args{
                .asset1Out = XRP(100),
                .asset2Out = BAD(100),
                .err = ter(temBAD_CURRENCY),
            };
            ammAlice.withdraw(args);
        });

        testAMM([&](AMM& ammAlice, Env& env) {
            Json::Value jv;
            jv[jss::TransactionType] = jss::AMMWithdraw;
            jv[jss::Flags] = tfLimitLPToken;
            jv[jss::Account] = alice.human();
            ammAlice.setTokens(jv);
            XRP(100).value().setJson(jv[jss::Amount]);
            USD(100).value().setJson(jv[jss::EPrice]);
            env(jv, ter(temBAD_AMM_TOKENS));
        });
    }

    void
    testFixOverflowOffer(FeatureBitset featuresInitial)
    {
        using namespace jtx;
        using namespace std::chrono;
        FeatureBitset const all{featuresInitial};

        std::string logs;

        Account const gatehub{"gatehub"};
        Account const bitstamp{"bitstamp"};
        Account const trader{"trader"};
        auto const usdGH = gatehub["USD"];
        auto const btcGH = gatehub["BTC"];
        auto const usdBIT = bitstamp["USD"];

        struct InputSet
        {
            char const* testCase;
            double const poolUsdBIT;
            double const poolUsdGH;
            sendmax const sendMaxUsdBIT;
            STAmount const sendUsdGH;
            STAmount const failUsdGH;
            STAmount const failUsdGHr;
            STAmount const failUsdBIT;
            STAmount const failUsdBITr;
            STAmount const goodUsdGH;
            STAmount const goodUsdGHr;
            STAmount const goodUsdBIT;
            STAmount const goodUsdBITr;
            IOUAmount const lpTokenBalance;
            std::optional<IOUAmount> const lpTokenBalanceAlt = std::nullopt;
            double const offer1BtcGH = 0.1;
            double const offer2BtcGH = 0.1;
            double const offer2UsdGH = 1;
            double const rateBIT = 0.0;
            double const rateGH = 0.0;
        };

        using uint64_t = std::uint64_t;

        for (auto const& input : {
                 InputSet{
                     .testCase = "Test Fix Overflow Offer",                   //
                     .poolUsdBIT = 3,                                         //
                     .poolUsdGH = 273,                                        //
                     .sendMaxUsdBIT{usdBIT(50)},                              //
                     .sendUsdGH{usdGH, uint64_t(272'455089820359), -12},      //
                     .failUsdGH = STAmount{0},                                //
                     .failUsdGHr = STAmount{0},                               //
                     .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14},   //
                     .failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14},  //
                     .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13},      //
                     .goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13},     //
                     .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15},   //
                     .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15},  //
                     .lpTokenBalance = {28'61817604250837, -14},              //
                     .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14},  //
                     .offer1BtcGH = 0.1,                                      //
                     .offer2BtcGH = 0.1,                                      //
                     .offer2UsdGH = 1,                                        //
                     .rateBIT = 1.15,                                         //
                     .rateGH = 1.2,                                           //
                 },
                 InputSet{
                     .testCase = "Overflow test {1, 100, 0.111}",           //
                     .poolUsdBIT = 1,                                       //
                     .poolUsdGH = 100,                                      //
                     .sendMaxUsdBIT{usdBIT(0.111)},                         //
                     .sendUsdGH{usdGH, 100},                                //
                     .failUsdGH = STAmount{0},                              //
                     .failUsdGHr = STAmount{0},                             //
                     .failUsdBIT{usdBIT, uint64_t(1'111), -3},              //
                     .failUsdBITr{usdBIT, uint64_t(1'111), -3},             //
                     .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14},   //
                     .goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14},  //
                     .goodUsdBIT{usdBIT, uint64_t(1'111), -3},              //
                     .goodUsdBITr{usdBIT, uint64_t(1'111), -3},             //
                     .lpTokenBalance{10, 0},                                //
                     .offer1BtcGH = 1e-5,                                   //
                     .offer2BtcGH = 1,                                      //
                     .offer2UsdGH = 1e-5,                                   //
                     .rateBIT = 0,                                          //
                     .rateGH = 0,                                           //
                 },
                 InputSet{
                     .testCase = "Overflow test {1, 100, 1.00}",            //
                     .poolUsdBIT = 1,                                       //
                     .poolUsdGH = 100,                                      //
                     .sendMaxUsdBIT{usdBIT(1.00)},                          //
                     .sendUsdGH{usdGH, 100},                                //
                     .failUsdGH = STAmount{0},                              //
                     .failUsdGHr = STAmount{0},                             //
                     .failUsdBIT{usdBIT, uint64_t(2), 0},                   //
                     .failUsdBITr{usdBIT, uint64_t(2), 0},                  //
                     .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14},   //
                     .goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14},  //
                     .goodUsdBIT{usdBIT, uint64_t(2), 0},                   //
                     .goodUsdBITr{usdBIT, uint64_t(2), 0},                  //
                     .lpTokenBalance{10, 0},                                //
                     .offer1BtcGH = 1e-5,                                   //
                     .offer2BtcGH = 1,                                      //
                     .offer2UsdGH = 1e-5,                                   //
                     .rateBIT = 0,                                          //
                     .rateGH = 0,                                           //
                 },
                 InputSet{
                     .testCase = "Overflow test {1, 100, 4.6432}",            //
                     .poolUsdBIT = 1,                                         //
                     .poolUsdGH = 100,                                        //
                     .sendMaxUsdBIT{usdBIT(4.6432)},                          //
                     .sendUsdGH{usdGH, 100},                                  //
                     .failUsdGH = STAmount{0},                                //
                     .failUsdGHr = STAmount{0},                               //
                     .failUsdBIT{usdBIT, uint64_t(5'6432), -4},               //
                     .failUsdBITr{usdBIT, uint64_t(5'6432), -4},              //
                     .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14},     //
                     .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14},    //
                     .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15},   //
                     .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15},  //
                     .lpTokenBalance{10, 0},                                  //
                     .offer1BtcGH = 1e-5,                                     //
                     .offer2BtcGH = 1,                                        //
                     .offer2UsdGH = 1e-5,                                     //
                     .rateBIT = 0,                                            //
                     .rateGH = 0,                                             //
                 },
                 InputSet{
                     .testCase = "Overflow test {1, 100, 10}",                //
                     .poolUsdBIT = 1,                                         //
                     .poolUsdGH = 100,                                        //
                     .sendMaxUsdBIT{usdBIT(10)},                              //
                     .sendUsdGH{usdGH, 100},                                  //
                     .failUsdGH = STAmount{0},                                //
                     .failUsdGHr = STAmount{0},                               //
                     .failUsdBIT{usdBIT, uint64_t(11), 0},                    //
                     .failUsdBITr{usdBIT, uint64_t(11), 0},                   //
                     .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14},     //
                     .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14},    //
                     .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15},   //
                     .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15},  //
                     .lpTokenBalance{10, 0},                                  //
                     .offer1BtcGH = 1e-5,                                     //
                     .offer2BtcGH = 1,                                        //
                     .offer2UsdGH = 1e-5,                                     //
                     .rateBIT = 0,                                            //
                     .rateGH = 0,                                             //
                 },
                 InputSet{
                     .testCase = "Overflow test {50, 100, 5.55}",          //
                     .poolUsdBIT = 50,                                     //
                     .poolUsdGH = 100,                                     //
                     .sendMaxUsdBIT{usdBIT(5.55)},                         //
                     .sendUsdGH{usdGH, 100},                               //
                     .failUsdGH = STAmount{0},                             //
                     .failUsdGHr = STAmount{0},                            //
                     .failUsdBIT{usdBIT, uint64_t(55'55), -2},             //
                     .failUsdBITr{usdBIT, uint64_t(55'55), -2},            //
                     .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14},  //
                     .goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13},  //
                     .goodUsdBIT{usdBIT, uint64_t(55'55), -2},             //
                     .goodUsdBITr{usdBIT, uint64_t(55'55), -2},            //
                     .lpTokenBalance{uint64_t(70'71067811865475), -14},    //
                     .offer1BtcGH = 1e-5,                                  //
                     .offer2BtcGH = 1,                                     //
                     .offer2UsdGH = 1e-5,                                  //
                     .rateBIT = 0,                                         //
                     .rateGH = 0,                                          //
                 },
                 InputSet{
                     .testCase = "Overflow test {50, 100, 50.00}",          //
                     .poolUsdBIT = 50,                                      //
                     .poolUsdGH = 100,                                      //
                     .sendMaxUsdBIT{usdBIT(50.00)},                         //
                     .sendUsdGH{usdGH, 100},                                //
                     .failUsdGH{usdGH, uint64_t(52'94379354424081), -14},   //
                     .failUsdGHr{usdGH, uint64_t(52'94379354424092), -14},  //
                     .failUsdBIT{usdBIT, uint64_t(100), 0},                 //
                     .failUsdBITr{usdBIT, uint64_t(100), 0},                //
                     .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14},   //
                     .goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14},  //
                     .goodUsdBIT{usdBIT, uint64_t(100), 0},                 //
                     .goodUsdBITr{usdBIT, uint64_t(100), 0},                //
                     .lpTokenBalance{uint64_t(70'71067811865475), -14},     //
                     .offer1BtcGH = 1e-5,                                   //
                     .offer2BtcGH = 1,                                      //
                     .offer2UsdGH = 1e-5,                                   //
                     .rateBIT = 0,                                          //
                     .rateGH = 0,                                           //
                 },
                 InputSet{
                     .testCase = "Overflow test {50, 100, 232.16}",           //
                     .poolUsdBIT = 50,                                        //
                     .poolUsdGH = 100,                                        //
                     .sendMaxUsdBIT{usdBIT(232.16)},                          //
                     .sendUsdGH{usdGH, 100},                                  //
                     .failUsdGH = STAmount{0},                                //
                     .failUsdGHr = STAmount{0},                               //
                     .failUsdBIT{usdBIT, uint64_t(282'16), -2},               //
                     .failUsdBITr{usdBIT, uint64_t(282'16), -2},              //
                     .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14},     //
                     .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14},    //
                     .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13},   //
                     .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13},  //
                     .lpTokenBalance{70'71067811865475, -14},                 //
                     .offer1BtcGH = 1e-5,                                     //
                     .offer2BtcGH = 1,                                        //
                     .offer2UsdGH = 1e-5,                                     //
                     .rateBIT = 0,                                            //
                     .rateGH = 0,                                             //
                 },
                 InputSet{
                     .testCase = "Overflow test {50, 100, 500}",              //
                     .poolUsdBIT = 50,                                        //
                     .poolUsdGH = 100,                                        //
                     .sendMaxUsdBIT{usdBIT(500)},                             //
                     .sendUsdGH{usdGH, 100},                                  //
                     .failUsdGH = STAmount{0},                                //
                     .failUsdGHr = STAmount{0},                               //
                     .failUsdBIT{usdBIT, uint64_t(550), 0},                   //
                     .failUsdBITr{usdBIT, uint64_t(550), 0},                  //
                     .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14},     //
                     .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14},    //
                     .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13},   //
                     .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13},  //
                     .lpTokenBalance{70'71067811865475, -14},                 //
                     .offer1BtcGH = 1e-5,                                     //
                     .offer2BtcGH = 1,                                        //
                     .offer2UsdGH = 1e-5,                                     //
                     .rateBIT = 0,                                            //
                     .rateGH = 0,                                             //
                 },
             })
        {
            testcase(input.testCase);
            for (auto const& features : {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all})
            {
                Env env(*this, features, std::make_unique<CaptureLogs>(&logs));

                env.fund(XRP(5'000), gatehub, bitstamp, trader);
                env.close();

                if (input.rateGH != 0.0)
                    env(rate(gatehub, input.rateGH));
                if (input.rateBIT != 0.0)
                    env(rate(bitstamp, input.rateBIT));

                env(trust(trader, usdGH(10'000'000)));
                env(trust(trader, usdBIT(10'000'000)));
                env(trust(trader, btcGH(10'000'000)));
                env.close();

                env(pay(gatehub, trader, usdGH(100'000)));
                env(pay(gatehub, trader, btcGH(100'000)));
                env(pay(bitstamp, trader, usdBIT(100'000)));
                env.close();

                AMM amm{env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)};
                env.close();

                IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance();

                env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH)));
                env(offer(trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH)));
                env.close();

                env(pay(trader, trader, input.sendUsdGH),
                    path(~usdGH),
                    path(~btcGH, ~usdGH),
                    sendmax(input.sendMaxUsdBIT),
                    txflags(tfPartialPayment));
                env.close();

                auto const failUsdGH = features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH;
                auto const failUsdBIT = features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT;
                auto const goodUsdGH = features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH;
                auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT;
                auto const lpTokenBalance = [&] {
                    if (not env.enabled(fixAMMv1_3))
                        return input.lpTokenBalance;

                    return input.lpTokenBalanceAlt.value_or(input.lpTokenBalance);
                }();

                if (!features[fixAMMOverflowOffer])
                {
                    BEAST_EXPECT(amm.expectBalances(failUsdGH, failUsdBIT, lpTokenBalance));
                }
                else
                {
                    BEAST_EXPECT(amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance));

                    // Invariant: LPToken balance must not change in a
                    // payment or a swap transaction
                    BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance);

                    // Invariant: The square root of (product of the pool
                    // balances) must be at least the LPTokenBalance
                    Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT);

                    // Include a tiny tolerance for the test cases using
                    //   .goodUsdGH{usdGH, uint64_t(35'44113971506987),
                    //   -14}, .goodUsdBIT{usdBIT,
                    //   uint64_t(2'821579689703915), -15},
                    // These two values multiply
                    // to 99.99999999999994227040383754105 which gets
                    // internally rounded to 100, due to representation
                    // error.
                    BEAST_EXPECT((sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance));
                }
            }
        }
    }

    void
    testSwapRounding()
    {
        testcase("swapRounding");
        using namespace jtx;

        STAmount const xrpPool{XRP, UINT64_C(51600'000981)};
        STAmount const iouPool{USD, UINT64_C(803040'9987141784), -10};

        STAmount const xrpBob{XRP, UINT64_C(1092'878933)};
        STAmount const iouBob{USD, UINT64_C(3'988035892323031), -28};  // 3.9...e-13

        testAMM(
            [&](AMM& amm, Env& env) {
                // Check our AMM starting conditions.
                auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD);

                // Set Bob's starting conditions.
                env.fund(xrpBob, bob);
                env.trust(USD(1'000'000), bob);
                env(pay(gw, bob, iouBob));
                env.close();

                env(offer(bob, XRP(6300), USD(100'000)));
                env.close();

                // Assert that AMM is unchanged.
                BEAST_EXPECT(amm.expectBalances(xrpBegin, iouBegin, amm.tokens()));
            },
            {{xrpPool, iouPool}},
            889,
            std::nullopt,
            {testable_amendments() | fixAMMv1_1});
    }

    void
    testFixAMMOfferBlockedByLOB(FeatureBitset features)
    {
        testcase("AMM Offer Blocked By LOB");
        using namespace jtx;

        // Low quality LOB offer blocks AMM liquidity

        // USD/XRP crosses AMM
        {
            Env env(*this, features);

            fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
            // This offer blocks AMM offer in pre-amendment
            env(offer(alice, XRP(1), USD(0.01)));
            env.close();

            AMM amm(env, gw, XRP(200'000), USD(100'000));

            // The offer doesn't cross AMM in pre-amendment code
            // It crosses AMM in post-amendment code
            env(offer(carol, USD(0.49), XRP(1)));
            env.close();

            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(amm.expectBalances(XRP(200'000), USD(100'000), amm.tokens()));
                BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
                // Carol's offer is blocked by alice's offer
                BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{USD(0.49), XRP(1)}}}));
            }
            else
            {
                BEAST_EXPECT(
                    amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
                BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
                // Carol's offer crosses AMM
                BEAST_EXPECT(expectOffers(env, carol, 0));
            }
        }

        // There is no blocking offer, the same AMM liquidity is consumed
        // pre- and post-amendment.
        {
            Env env(*this, features);

            fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
            // There is no blocking offer
            // env(offer(alice, XRP(1), USD(0.01)));

            AMM amm(env, gw, XRP(200'000), USD(100'000));

            // The offer crosses AMM
            env(offer(carol, USD(0.49), XRP(1)));
            env.close();

            // The same result as with the blocking offer
            BEAST_EXPECT(
                amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
            // Carol's offer crosses AMM
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }

        // XRP/USD crosses AMM
        {
            Env env(*this, features);
            fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});

            // This offer blocks AMM offer in pre-amendment
            // It crosses AMM in post-amendment code
            env(offer(bob, USD(1), XRPAmount(500)));
            env.close();
            AMM amm(env, alice, XRP(1'000), USD(500));
            env(offer(carol, XRP(100), USD(55)));
            env.close();
            if (!features[fixAMMv1_1])
            {
                BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
                BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
                BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
            }
            else
            {
                BEAST_EXPECT(amm.expectBalances(
                    XRPAmount(909'090'909),
                    STAmount{USD, UINT64_C(550'000000055), -9},
                    amm.tokens()));
                BEAST_EXPECT(expectOffers(
                    env,
                    carol,
                    1,
                    {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
                BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
            }
        }

        // There is no blocking offer, the same AMM liquidity is consumed
        // pre- and post-amendment.
        {
            Env env(*this, features);
            fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});

            AMM amm(env, alice, XRP(1'000), USD(500));
            env(offer(carol, XRP(100), USD(55)));
            env.close();
            BEAST_EXPECT(amm.expectBalances(
                XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
            BEAST_EXPECT(expectOffers(
                env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
        }
    }

    void
    testLPTokenBalance(FeatureBitset features)
    {
        testcase("LPToken Balance");
        using namespace jtx;

        // Last Liquidity Provider is the issuer of one token
        {
            std::string logs;
            Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
            fund(env, gw, {alice, carol}, XRP(1'000'000'000), {USD(1'000'000'000)});
            AMM amm(env, gw, XRP(2), USD(1));
            amm.deposit(alice, IOUAmount{1'876123487565916, -15});
            amm.deposit(carol, IOUAmount{1'000'000});
            amm.withdrawAll(alice);
            BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0}));
            amm.withdrawAll(carol);
            BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0}));
            auto const lpToken =
                getAccountLines(env, gw, amm.lptIssue())[jss::lines][0u][jss::balance];
            auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
            BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
            if (!features[fixAMMv1_1])
            {
                amm.withdrawAll(gw, std::nullopt, ter(tecAMM_BALANCE));
                BEAST_EXPECT(amm.ammExists());
            }
            else
            {
                amm.withdrawAll(gw);
                BEAST_EXPECT(!amm.ammExists());
            }
        }

        // Last Liquidity Provider is the issuer of two tokens, or not
        // the issuer
        for (auto const& lp : {gw, bob})
        {
            Env env(*this, features);
            auto const ABC = gw["ABC"];
            fund(
                env,
                gw,
                {alice, carol, bob},
                XRP(1'000),
                {USD(1'000'000'000), ABC(1'000'000'000'000)});
            AMM amm(env, lp, ABC(2'000'000), USD(1));
            amm.deposit(alice, IOUAmount{1'876123487565916, -15});
            amm.deposit(carol, IOUAmount{1'000'000});
            amm.withdrawAll(alice);
            amm.withdrawAll(carol);
            auto const lpToken =
                getAccountLines(env, lp, amm.lptIssue())[jss::lines][0u][jss::balance];
            auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
            BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
            if (!features[fixAMMv1_1])
            {
                amm.withdrawAll(lp, std::nullopt, ter(tecAMM_BALANCE));
                BEAST_EXPECT(amm.ammExists());
            }
            else
            {
                amm.withdrawAll(lp);
                BEAST_EXPECT(!amm.ammExists());
            }
        }

        // More than one Liquidity Provider
        // XRP/IOU
        {
            Env env(*this, features);
            fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
            AMM amm(env, gw, XRP(10), USD(10));
            amm.deposit(alice, 1'000);
            auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
            BEAST_EXPECT(res && !res.value());
            res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
            BEAST_EXPECT(res && !res.value());
        }
        // IOU/IOU, issuer of both IOU
        {
            Env env(*this, features);
            fund(env, gw, {alice}, XRP(1'000), {USD(1'000), EUR(1'000)});
            AMM amm(env, gw, EUR(10), USD(10));
            amm.deposit(alice, 1'000);
            auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
            BEAST_EXPECT(res && !res.value());
            res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
            BEAST_EXPECT(res && !res.value());
        }
        // IOU/IOU, issuer of one IOU
        {
            Env env(*this, features);
            Account const gw1("gw1");
            auto const YAN = gw1["YAN"];
            fund(env, gw, {gw1}, XRP(1'000), {USD(1'000)});
            fund(env, gw1, {gw}, XRP(1'000), {YAN(1'000)}, Fund::IOUOnly);
            AMM amm(env, gw1, YAN(10), USD(10));
            amm.deposit(gw, 1'000);
            auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
            BEAST_EXPECT(res && !res.value());
            res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1);
            BEAST_EXPECT(res && !res.value());
        }
    }

    void
    testAMMClawback(FeatureBitset features)
    {
        testcase("test clawback from AMM account");
        using namespace jtx;

        // Issuer has clawback enabled
        Env env(*this, features);
        env.fund(XRP(1'000), gw);
        env(fset(gw, asfAllowTrustLineClawback));
        fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
        env.close();

        // If featureAMMClawback is not enabled, AMMCreate is not allowed for
        // clawback-enabled issuer
        if (!features[featureAMMClawback])
        {
            AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
            AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION));
            env(fclear(gw, asfAllowTrustLineClawback));
            env.close();
            // Can't be cleared
            AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
        }
        // If featureAMMClawback is enabled, AMMCreate is allowed for
        // clawback-enabled issuer. Clawback from the AMM Account is not
        // allowed, which will return tecAMM_ACCOUNT or tecPSEUDO_ACCOUNT,
        // depending on whether SingleAssetVault is enabled. We can only use
        // AMMClawback transaction to claw back from AMM Account.
        else
        {
            AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS));
            AMM amm1(env, alice, USD(100), XRP(200), ter(tecDUPLICATE));

            // Construct the amount being clawed back using AMM account.
            // By doing this, we make the clawback transaction's Amount field's
            // subfield `issuer` to be the AMM account, which means
            // we are clawing back from an AMM account. This should return an
            // error because regular Clawback transaction is not
            // allowed for clawing back from an AMM account. Please notice the
            // `issuer` subfield represents the account being clawed back, which
            // is confusing.
            auto const error =
                features[featureSingleAssetVault] ? ter{tecPSEUDO_ACCOUNT} : ter{tecAMM_ACCOUNT};
            Issue usd(USD.issue().currency, amm.ammAccount());
            auto amount = amountFromString(usd, "10");
            env(claw(gw, amount), error);
        }
    }

    void
    testAMMDepositWithFrozenAssets(FeatureBitset features)
    {
        testcase("test AMMDeposit with frozen assets");
        using namespace jtx;

        // This lambda function is used to create trustlines
        // between gw and alice, and create an AMM account.
        // And also test the callback function.
        auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm)> cb) {
            env.fund(XRP(1'000), gw);
            fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
            env.close();
            AMM amm(env, alice, XRP(100), USD(100), ter(tesSUCCESS));
            env(trust(gw, alice["USD"](0), tfSetFreeze));
            cb(amm);
        };

        // Deposit two assets, one of which is frozen,
        // then we should get tecFROZEN error.
        {
            Env env(*this, features);
            testAMMDeposit(env, [&](AMM& amm) {
                amm.deposit(alice, USD(100), XRP(100), std::nullopt, tfTwoAsset, ter(tecFROZEN));
            });
        }

        // Deposit one asset, which is the frozen token,
        // then we should get tecFROZEN error.
        {
            Env env(*this, features);
            testAMMDeposit(env, [&](AMM& amm) {
                amm.deposit(
                    alice, USD(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tecFROZEN));
            });
        }

        if (features[featureAMMClawback])
        {
            // Deposit one asset which is not the frozen token,
            // but the other asset is frozen. We should get tecFROZEN error
            // when feature AMMClawback is enabled.
            Env env(*this, features);
            testAMMDeposit(env, [&](AMM& amm) {
                amm.deposit(
                    alice, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tecFROZEN));
            });
        }
        else
        {
            // Deposit one asset which is not the frozen token,
            // but the other asset is frozen. We will get tecSUCCESS
            // when feature AMMClawback is not enabled.
            Env env(*this, features);
            testAMMDeposit(env, [&](AMM& amm) {
                amm.deposit(
                    alice, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tesSUCCESS));
            });
        }
    }

    void
    testFixReserveCheckOnWithdrawal(FeatureBitset features)
    {
        testcase("Fix Reserve Check On Withdrawal");
        using namespace jtx;

        auto const err = features[fixAMMv1_2] ? ter(tecINSUFFICIENT_RESERVE) : ter(tesSUCCESS);

        auto test = [&](auto&& cb) {
            Env env(*this, features);
            auto const starting_xrp = reserve(env, 2) + env.current()->fees().base * 5;
            env.fund(starting_xrp, gw);
            env.fund(starting_xrp, alice);
            env.trust(USD(2'000), alice);
            env.close();
            env(pay(gw, alice, USD(2'000)));
            env.close();
            AMM amm(env, gw, EUR(1'000), USD(1'000));
            amm.deposit(alice, USD(1));
            cb(amm);
        };

        // Equal withdraw
        test([&](AMM& amm) { amm.withdrawAll(alice, std::nullopt, err); });

        // Equal withdraw with a limit
        test([&](AMM& amm) {
            amm.withdraw(
                WithdrawArg{
                    .account = alice, .asset1Out = EUR(0.1), .asset2Out = USD(0.1), .err = err});
            amm.withdraw(
                WithdrawArg{
                    .account = alice, .asset1Out = USD(0.1), .asset2Out = EUR(0.1), .err = err});
        });

        // Single withdraw
        test([&](AMM& amm) {
            amm.withdraw(WithdrawArg{.account = alice, .asset1Out = EUR(0.1), .err = err});
            amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1)});
        });
    }

    void
    testFailedPseudoAccount()
    {
        using namespace test::jtx;

        auto const testCase = [&](std::string suffix, FeatureBitset features) {
            testcase("Fail pseudo-account allocation " + suffix);
            std::string logs;
            Env env{*this, features, std::make_unique<CaptureLogs>(&logs)};
            env.fund(XRP(30'000), gw, alice);
            env.close();
            env(trust(alice, gw["USD"](30'000), 0));
            env(pay(gw, alice, USD(10'000)));
            env.close();

            STAmount amount = XRP(10'000);
            STAmount amount2 = USD(10'000);
            auto const keylet = keylet::amm(amount.issue(), amount2.issue());
            for (int i = 0; i < 256; ++i)
            {
                AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);

                env(pay(env.master.id(), accountId, XRP(1000)),
                    seq(autofill),
                    fee(autofill),
                    sig(autofill));
            }

            AMM ammAlice(
                env,
                alice,
                amount,
                amount2,
                features[featureSingleAssetVault] ? ter{terADDRESS_COLLISION} : ter{tecDUPLICATE});
        };

        testCase("tecDUPLICATE", testable_amendments() - featureSingleAssetVault);
        testCase("terADDRESS_COLLISION", testable_amendments() | featureSingleAssetVault);
    }

    void
    testDepositAndWithdrawRounding(FeatureBitset features)
    {
        testcase("Deposit and Withdraw Rounding V2");
        using namespace jtx;

        auto const XPM = gw["XPM"];
        STAmount xrpBalance{XRPAmount(692'614'492'126)};
        STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8};
        STAmount amount{XPM, UINT64_C(6'566'496939465400), -12};
        std::uint16_t tfee = 941;

        auto test = [&](auto&& cb, std::uint16_t tfee_) {
            Env env(*this, features);
            env.fund(XRP(1'000'000), gw);
            env.fund(XRP(1'000), alice);
            env(trust(alice, XPM(7'000)));
            env(pay(gw, alice, amount));

            AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_});
            // AMM LPToken balance required to replicate single deposit failure
            STAmount lptAMMBalance{amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6};
            auto const burn = IOUAmount{amm.getLPTokensBalance() - lptAMMBalance};
            // burn tokens to get to the required AMM state
            env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn}));
            cb(amm, env);
        };
        test(
            [&](AMM& amm, Env& env) {
                auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecUNFUNDED_AMM);
                amm.deposit(DepositArg{.account = alice, .asset1In = amount, .err = err});
            },
            tfee);
        test(
            [&](AMM& amm, Env& env) {
                auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM);
                auto const withdraw = STAmount{XPM, 1, -5};
                amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}});
                auto const [amount_, amount2_, lptAMM_] = amm.balances(XRP, XPM);
                if (!env.enabled(fixAMMv1_3))
                {
                    BEAST_EXPECT((amount2 - amount2_) > withdraw);
                }
                else
                {
                    BEAST_EXPECT((amount2 - amount2_) <= withdraw);
                }
            },
            0);
    }

    void
    invariant(jtx::AMM& amm, jtx::Env& env, std::string const& msg, bool shouldFail)
    {
        auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR);

        NumberMantissaScaleGuard sg(MantissaRange::small);
        NumberRoundModeGuard g(env.enabled(fixAMMv1_3) ? Number::upward : Number::getround());
        auto const res = root2(amount * amount2);

        if (shouldFail)
        {
            BEAST_EXPECT(res < lptBalance);
        }
        else
        {
            BEAST_EXPECT(res >= lptBalance);
        }
    }

    void
    testDepositRounding(FeatureBitset all)
    {
        testcase("Deposit Rounding");
        using namespace jtx;

        // Single asset deposit
        for (auto const& deposit :
             {STAmount(EUR, 1, 1),
              STAmount(EUR, 1, 2),
              STAmount(EUR, 1, 5),
              STAmount(EUR, 1, -3),  // fail
              STAmount(EUR, 1, -6),
              STAmount(EUR, 1, -9)})
        {
            testAMM(
                [&](AMM& ammAlice, Env& env) {
                    fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                    env.close();

                    ammAlice.deposit(DepositArg{.account = bob, .asset1In = deposit});
                    invariant(
                        ammAlice,
                        env,
                        "dep1",
                        deposit == STAmount{EUR, 1, -3} && !env.enabled(fixAMMv1_3));
                },
                {{GBP(30'000), EUR(30'000)}},
                0,
                std::nullopt,
                {all});
        }

        // Two-asset proportional deposit (1:1 pool ratio)
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                env.close();

                STAmount const depositEuro{EUR, UINT64_C(10'1234567890123456), -16};
                STAmount const depositGBP{GBP, UINT64_C(10'1234567890123456), -16};

                ammAlice.deposit(
                    DepositArg{.account = bob, .asset1In = depositEuro, .asset2In = depositGBP});
                invariant(ammAlice, env, "dep2", false);
            },
            {{GBP(30'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // Two-asset proportional deposit (1:3 pool ratio)
        for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9})
        {
            testAMM(
                [&](AMM& ammAlice, Env& env) {
                    fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                    env.close();

                    STAmount const depositEuro{EUR, 1, exponent};
                    STAmount const depositGBP{GBP, 1, exponent};

                    ammAlice.deposit(
                        DepositArg{
                            .account = bob, .asset1In = depositEuro, .asset2In = depositGBP});
                    invariant(ammAlice, env, "dep3", exponent != -3 && !env.enabled(fixAMMv1_3));
                },
                {{GBP(10'000), EUR(30'000)}},
                0,
                std::nullopt,
                {all});
        }

        // tfLPToken deposit
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                env.close();

                ammAlice.deposit(
                    DepositArg{.account = bob, .tokens = IOUAmount{10'1234567890123456, -16}});
                invariant(ammAlice, env, "dep4", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfOneAssetLPToken deposit
        for (auto const& tokens :
             {IOUAmount{1, -3},
              IOUAmount{1, -2},
              IOUAmount{1, -1},
              IOUAmount{1},
              IOUAmount{10},
              IOUAmount{100},
              IOUAmount{1'000},
              IOUAmount{10'000}})
        {
            testAMM(
                [&](AMM& ammAlice, Env& env) {
                    fund(
                        env,
                        gw,
                        {bob},
                        XRP(10'000'000),
                        {GBP(100'000), EUR(1'000'000)},
                        Fund::Acct);
                    env.close();

                    ammAlice.deposit(
                        DepositArg{
                            .account = bob, .tokens = tokens, .asset1In = STAmount{EUR, 1, 6}});
                    invariant(ammAlice, env, "dep5", false);
                },
                {{GBP(7'000), EUR(30'000)}},
                0,
                std::nullopt,
                {all});
        }

        // Single deposit with EP not exceeding specified:
        // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut)
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                env.close();

                ammAlice.deposit(bob, GBP(1'000), std::nullopt, STAmount{GBP, 5});
                invariant(ammAlice, env, "dep6", false);
            },
            {{GBP(30'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});
    }

    void
    testWithdrawRounding(FeatureBitset all)
    {
        testcase("Withdraw Rounding");

        using namespace jtx;

        // tfLPToken mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(alice, 1'000);
                invariant(ammAlice, env, "with1", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfWithdrawAll mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(WithdrawArg{.account = alice, .flags = tfWithdrawAll});
                invariant(ammAlice, env, "with2", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfTwoAsset withdraw mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(
                    WithdrawArg{
                        .account = alice,
                        .asset1Out = STAmount{GBP, 3'500},
                        .asset2Out = STAmount{EUR, 15'000},
                        .flags = tfTwoAsset});
                invariant(ammAlice, env, "with3", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfSingleAsset withdraw mode
        // Note: This test fails with 0 trading fees, but doesn't fail if
        // trading fees is set to 1'000 -- I suspect the compound operations
        // in AMMHelpers.cpp:withdrawByTokens compensate for the rounding
        // errors
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(
                    WithdrawArg{
                        .account = alice,
                        .asset1Out = STAmount{GBP, 1'234},
                        .flags = tfSingleAsset});
                invariant(ammAlice, env, "with4", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfOneAssetWithdrawAll mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
                env.close();

                ammAlice.deposit(DepositArg{.account = bob, .asset1In = STAmount{GBP, 3'456}});

                ammAlice.withdraw(
                    WithdrawArg{
                        .account = bob,
                        .asset1Out = STAmount{GBP, 1'000},
                        .flags = tfOneAssetWithdrawAll});
                invariant(ammAlice, env, "with5", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfOneAssetLPToken mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(
                    WithdrawArg{
                        .account = alice,
                        .tokens = 1'000,
                        .asset1Out = STAmount{GBP, 100},
                        .flags = tfOneAssetLPToken});
                invariant(ammAlice, env, "with6", false);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});

        // tfLimitLPToken mode
        testAMM(
            [&](AMM& ammAlice, Env& env) {
                ammAlice.withdraw(
                    WithdrawArg{
                        .account = alice,
                        .asset1Out = STAmount{GBP, 100},
                        .maxEP = IOUAmount{2},
                        .flags = tfLimitLPToken});
                invariant(ammAlice, env, "with7", true);
            },
            {{GBP(7'000), EUR(30'000)}},
            0,
            std::nullopt,
            {all});
    }

    void
    run() override
    {
        FeatureBitset const all{testable_amendments()};
        testInvalidInstance();
        testInstanceCreate();
        testInvalidDeposit(all);
        testInvalidDeposit(all - featureAMMClawback);
        testDeposit();
        testInvalidWithdraw();
        testWithdraw();
        testInvalidFeeVote();
        testFeeVote();
        testInvalidBid();
        testBid(all);
        testBid(all - fixAMMv1_3);
        testBid(all - fixAMMv1_1 - fixAMMv1_3);
        testInvalidAMMPayment();
        testBasicPaymentEngine(all);
        testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3);
        testBasicPaymentEngine(all - fixReducedOffersV2);
        testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2);
        testAMMTokens();
        testAmendment();
        testFlags();
        testRippling();
        testAMMAndCLOB(all);
        testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
        testTradingFee(all);
        testTradingFee(all - fixAMMv1_3);
        testTradingFee(all - fixAMMv1_1 - fixAMMv1_3);
        testAdjustedTokens(all);
        testAdjustedTokens(all - fixAMMv1_3);
        testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3);
        testAutoDelete();
        testClawback();
        testAMMID();
        testSelection(all);
        testSelection(all - fixAMMv1_1 - fixAMMv1_3);
        testFixDefaultInnerObj();
        testMalformed();
        testFixOverflowOffer(all);
        testFixOverflowOffer(all - fixAMMv1_3);
        testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
        testSwapRounding();
        testFixChangeSpotPriceQuality(all);
        testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
        testFixAMMOfferBlockedByLOB(all);
        testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
        testLPTokenBalance(all);
        testLPTokenBalance(all - fixAMMv1_3);
        testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3);
        testAMMClawback(all);
        testAMMClawback(all - featureSingleAssetVault);
        testAMMClawback(all - featureAMMClawback - featureSingleAssetVault);
        testAMMClawback(all - featureAMMClawback);
        testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
        testAMMDepositWithFrozenAssets(all);
        testAMMDepositWithFrozenAssets(all - featureAMMClawback);
        testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
        testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
        testFixReserveCheckOnWithdrawal(all);
        testFixReserveCheckOnWithdrawal(all - fixAMMv1_2);
        testDepositAndWithdrawRounding(all);
        testDepositAndWithdrawRounding(all - fixAMMv1_3);
        testDepositRounding(all);
        testDepositRounding(all - fixAMMv1_3);
        testWithdrawRounding(all);
        testWithdrawRounding(all - fixAMMv1_3);
        testFailedPseudoAccount();
    }
};

BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, xrpl, 1);

}  // namespace test
}  // namespace xrpl
