diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index b6828fab773..12c0cc64cc2 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -4889,6 +4889,280 @@ struct AMM_test : public jtx::AMMTest false); } + void + testFixOverflowOffer() + { + using namespace jtx; + using namespace std::chrono; + FeatureBitset const all{supported_amendments()}; + + 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 failUsdBIT; + STAmount const goodUsdGH; + STAmount const goodUsdBIT; + IOUAmount const lpTokenBalance; + 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}, // + .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, // + .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, // + .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // + .lpTokenBalance = {28'61817604250837, -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}, // + .failUsdBIT{usdBIT, uint64_t(1'111), -3}, // + .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, // + .goodUsdBIT{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}, // + .failUsdBIT{usdBIT, uint64_t(2), 0}, // + .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, // + .goodUsdBIT{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}, // + .failUsdBIT{usdBIT, uint64_t(5'6432), -4}, // + .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // + .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -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}, // + .failUsdBIT{usdBIT, uint64_t(11), 0}, // + .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // + .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -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}, // + .failUsdBIT{usdBIT, uint64_t(55'55), -2}, // + .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, // + .goodUsdBIT{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}, // + .failUsdBIT{usdBIT, uint64_t(100), 0}, // + .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, // + .goodUsdBIT{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}, // + .failUsdBIT{usdBIT, uint64_t(282'16), -2}, // + .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // + .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -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}, // + .failUsdBIT{usdBIT, uint64_t(550), 0}, // + .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, // + .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -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, all | fixAMMOverflowOffer}) + { + Env env(*this, features); + + 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(); + if (!features[fixAMMOverflowOffer]) + BEAST_EXPECT(amm.expectBalances( + input.failUsdGH, + input.failUsdBIT, + input.lpTokenBalance)); + else + { + BEAST_EXPECT(amm.expectBalances( + input.goodUsdGH, + input.goodUsdBIT, + input.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(input.goodUsdGH * input.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 testCore() { @@ -4916,6 +5190,7 @@ struct AMM_test : public jtx::AMMTest testAMMID(); testSelection(); testFixDefaultInnerObj(); + testFixOverflowOffer(); } void