fix(AMM): prevent orphaned objects, inconsistent ledger state: (#4626)

When an AMM account is deleted, the owner directory entries must be
deleted in order to ensure consistent ledger state.

* When deleting AMM account:
  * Clean up AMM owner dir, linking AMM account and AMM object
  * Delete trust lines to AMM
* Disallow `CheckCreate` to AMM accounts
  * AMM cannot cash a check
* Constrain entries in AuthAccounts array to be accounts
  * AuthAccounts is an array of objects for the AMMBid transaction
* SetTrust (TrustSet): Allow on AMM only for LP tokens
  * If the destination is an AMM account and the trust line doesn't
    exist, then:
    * If the asset is not the AMM LP token, then fail the tx with
      `tecNO_PERMISSION`
    * If the AMM is in empty state, then fail the tx with `tecAMM_EMPTY`
      * This disallows trustlines to AMM in empty state
* Add AMMID to AMM root account
  * Remove lsfAMM flag and use sfAMMID instead
* Remove owner dir entry for ltAMM
* Add `AMMDelete` transaction type to handle amortized deletion
  * Limit number of trust lines to delete on final withdraw + AMMDelete
  * Put AMM in empty state when LPTokens is 0 upon final withdraw
  * Add `tfTwoAssetIfEmpty` deposit option in AMM empty state
  * Fail all AMM transactions in AMM empty state except special deposit
  * Add `tecINCOMPLETE` to indicate that not all AMM trust lines are
    deleted (i.e. partial deletion)
    * This is handled in Transactor similar to deleted offers
  * Fail AMMDelete with `tecINTERNAL` if AMM root account is nullptr
  * Don't validate for invalid asset pair in AMMDelete
* AMMWithdraw deletes AMM trust lines and AMM account/object only if the
  number of trust lines is less than max
  * Current `maxDeletableAMMTrustLines` = 512
  * Check no directory left after AMM trust lines are deleted
  * Enable partial trustline deletion in AMMWithdraw
* Add `tecAMM_NOT_EMPTY` to fail any transaction that expects an AMM in
  empty state
* Clawback considerations
  * Disallow clawback out of AMM account
  * Disallow AMM create if issuer can claw back

This patch applies to the AMM implementation in #4294.

Acknowledgements:
Richard Holland and Nik Bougalis for responsibly disclosing this issue.

Bug Bounties and Responsible Disclosures:
We welcome reviews of the project code and urge researchers to
responsibly disclose any issues they may find.

To report a bug, please send a detailed report to:

    bugs@xrpl.org

Signed-off-by: Manoj Doshi <mdoshi@ripple.com>
This commit is contained in:
Gregory Tsipenyuk
2023-07-17 08:05:11 -04:00
committed by Manoj Doshi
parent 5530a0b665
commit 58f7aecb0b
56 changed files with 1539 additions and 250 deletions

View File

@@ -27,7 +27,6 @@
#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
#include <test/jtx/sendmax.h>
@@ -109,6 +108,15 @@ private:
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);
}
void
@@ -392,6 +400,21 @@ private:
AMM ammAlice1(
env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
}
// Issuer has clawback enabled
{
Env env(*this);
env.fund(XRP(1'000), gw);
env(fset(gw, asfAllowTrustLineClawback));
fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
env.close();
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));
}
}
void
@@ -416,96 +439,167 @@ private:
std::optional<std::uint32_t>,
std::optional<STAmount>,
std::optional<STAmount>,
std::optional<STAmount>>>
std::optional<STAmount>,
std::optional<std::uint16_t>>>
invalidOptions = {
// flags, tokens, asset1In, asset2in, EPrice
{tfLPToken, 1'000, std::nullopt, USD(100), std::nullopt},
{tfLPToken, 1'000, XRP(100), std::nullopt, std::nullopt},
// flags, tokens, asset1In, asset2in, EPrice, tfee
{tfLPToken,
1'000,
std::nullopt,
std::nullopt,
STAmount{USD, 1, -1}},
{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,
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}},
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}},
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}},
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}},
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,
STAmount{USD, 1, -1}}};
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},
};
for (auto const& it : invalidOptions)
{
ammAlice.deposit(
@@ -517,6 +611,7 @@ private:
std::get<0>(it),
std::nullopt,
std::nullopt,
std::get<5>(it),
ter(temMALFORMED));
}
@@ -543,6 +638,7 @@ private:
std::nullopt,
{{iss1, iss2}},
std::nullopt,
std::nullopt,
ter(terNO_AMM));
}
@@ -617,6 +713,7 @@ private:
std::nullopt,
std::nullopt,
seq(1),
std::nullopt,
ter(terNO_ACCOUNT));
// Invalid AMM
@@ -629,6 +726,7 @@ private:
std::nullopt,
{{USD, GBP}},
std::nullopt,
std::nullopt,
ter(terNO_AMM));
// Single deposit: 100000 tokens worth of USD
@@ -642,6 +740,7 @@ private:
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Single deposit: 100000 tokens worth of XRP
@@ -655,6 +754,7 @@ private:
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Deposit amount is invalid
@@ -689,6 +789,15 @@ private:
std::nullopt,
std::nullopt,
ter(tecAMM_INVALID_TOKENS));
// Deposit non-empty AMM
ammAlice.deposit(
carol,
XRP(100),
USD(100),
std::nullopt,
tfTwoAssetIfEmpty,
ter(tecAMM_NOT_EMPTY));
});
// Invalid AMM
@@ -790,6 +899,7 @@ private:
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
@@ -809,6 +919,7 @@ private:
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecUNFUNDED_AMM));
});
@@ -884,6 +995,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
// min amounts can't be <= zero
ammAlice.deposit(
@@ -895,6 +1007,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
ammAlice.deposit(
carol,
@@ -905,6 +1018,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMOUNT));
// min amount bad currency
ammAlice.deposit(
@@ -916,6 +1030,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_CURRENCY));
// min amount bad token pair
ammAlice.deposit(
@@ -927,6 +1042,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
ammAlice.deposit(
carol,
@@ -937,6 +1053,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(temBAD_AMM_TOKENS));
});
@@ -952,6 +1069,7 @@ private:
tfLPToken,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
ammAlice.deposit(
carol,
@@ -962,6 +1080,7 @@ private:
tfLPToken,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Equal deposit by asset
ammAlice.deposit(
@@ -973,6 +1092,7 @@ private:
tfTwoAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
// Single deposit by asset
ammAlice.deposit(
@@ -984,6 +1104,7 @@ private:
tfSingleAsset,
std::nullopt,
std::nullopt,
std::nullopt,
ter(tecAMM_FAILED));
});
}
@@ -1250,6 +1371,16 @@ private:
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<
@@ -1770,9 +1901,19 @@ private:
// Withdraw all tokens.
testAMM([&](AMM& ammAlice, Env& env) {
env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000}));
// Can SetTrust 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(
@@ -2750,6 +2891,12 @@ private:
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) {
@@ -3566,8 +3713,7 @@ private:
info[jss::result][jss::account_data][jss::Flags].asUInt();
BEAST_EXPECT(
flags ==
(lsfAMM | lsfDisableMaster | lsfDefaultRipple |
lsfDepositAuth));
(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
});
}
@@ -3608,9 +3754,11 @@ private:
AMM amm(env, C, TSTA(5'000), TSTB(5'000));
auto const ammIss = Issue(TSTA.currency, amm.ammAccount());
env.trust(STAmount{ammIss, 10'000}, D);
// Can SetTrust 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()),
@@ -4132,6 +4280,515 @@ private:
});
}
void
testAutoDelete()
{
testcase("Auto Delete");
using namespace jtx;
FeatureBitset const all{supported_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,SetTrust failing with
// tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
amm.bid(
alice,
1000,
std::nullopt,
{},
std::nullopt,
std::nullopt,
std::nullopt,
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())));
}
}
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()
{
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);
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(expectLine(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);
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);
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)
{
if (rates.first == 1.5)
{
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(325'299461620749), -12},
STAmount{
USD,
UINT64_C(243'9745962155617),
-13}}}}));
}
}
BEAST_EXPECT(expectLine(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);
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)
{
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.
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));
}
}
}
// 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);
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(expectLine(env, bob, USD(2'100)));
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'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},
}}}));
}
}
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
testCore()
{
@@ -4154,6 +4811,10 @@ private:
testAMMAndCLOB();
testTradingFee();
testAdjustedTokens();
testAutoDelete();
testClawback();
testAMMID();
testSelection();
}
void
@@ -4166,4 +4827,4 @@ private:
BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, ripple, 1);
} // namespace test
} // namespace ripple
} // namespace ripple