CAP: To Be Assigned
Title: Automated Market Makers
Working Group:
Owner: Nicolas Barry <nic...@stellar.org>
Authors: OrbitLens <or...@stellar.expert>
Consulted: Jon Jove <j...@stellar.org>, Nikhil Saraf<nik...@stellar.org>, Phil Meng <ph...@stellar.org>, Leigh McCulloch <le...@stellar.org>, Tomer Weller <to...@stellar.org>
Status: Draft
Created: 2021-03-03
Protocol version: TBD
This proposal introduces liquidity pools and automated market makers on the
protocol level. AMMs rely on a mathematical formula to quote asset prices. A
liquidity pool is a ledger entry that contains funds deposited by users
(liquidity providers). In return for providing liquidity to the protocol, users
earn fees from trades. The described approach of the interleaved order execution
combines the liquidity of existing orderbooks with liquidity pools.
Orderbooks market-making (especially on-chain) may be quite tricky. It requires
trading bots that constantly track external asset prices and adjust on-chain
orders accordingly. In turn, this process results in endless offer adjustments
which clog ledger history.
Market makers need to provision liquidity and maintain inventories. For a few
trading pairs it is more or less straightforward but the ecosystem
expansion brings new assets, new trading pairs. Consequently, inventory
requirements increase, as well as the number of operations required to maintain
positions on all orderbooks.
On the other hand, automated market makers provide natural incentives for
liquidity crowdsourcing, making it much easier for ordinary users to participate
in the process while gaining interest on their long-term holdings.
Asset issuers don't need to wait until the token attracts a critical mass of
users. They can start making several trading pairs with a newly issued asset by
merely depositing tokens to the pool or engaging community users to provision
liquidity. This will certainly simplify the process of starting a new project on
Stellar, as well as provide a powerful marketing flywheel for early-stage
tokens.
The AMM concept implies that no third-party company holds user funds at any
point, and the algorithm itself doesn't rely on external data. Therefore,
potential regulatory risks are limited compared to the classic exchange design.
Liquidity pools don't store any complex information, don't require regular
position price adjustments, and work completely deterministically. From the
perspective of the on-chain execution, those characteristics offer much better
scalability compared to the existing DEX.
Proposed interleaved order execution on both the orderbook and liquidity pool
provides a familiar exchange experience in combination with the ability to have
on-chain limit orders. On the other hand, it fully incorporates all benefits of
shared liquidity pools, at the same time hiding the underlying technical details
from end-users. Users always get the best possible exchange price based on the
combined liquidity.
This proposal brings the concept of shared liquidity pools with automated market
making to the protocol. Users deposit funds to a pool providing liquidity to the
automated market maker execution engine which can quote asset prices based on an
algorithm that derives the price directly from the amounts of tokens deposited
to the pool.
Pool fees charged on every executed trade are accumulated in the pool,
increasing its liquidity. A user can withdraw the pool stake plus proportional
accrued interest from the pool. Collected interest incentivizes users to deposit
their funds to the pool, participating in the collective liquidity allocation.
LiquidityPoolEntry
and LiquidityStakeEntry
DepositPoolLiquidityOp
and WithdrawPoolLiquidityOp
LedgerHeader
extended with new settingsManageSellOfferOp
,ManageBuyOfferOp
, CreatePassiveSellOfferOp
, PathPaymentStrictReceiveOp
,PathPaymentStrictSendOp
--- a/src/xdr/Stellar-ledger-entries.x +++ b/src/xdr/Stellar-ledger-entries.x @@ -403,6 +403,43 @@ struct ClaimableBalanceEntry ext; }; +/* Contains information about current balances of the liquidity pool*/ +struct LiquidityPoolEntry +{ + uint32 poolID; // pool invariant identifier + Asset assetA; // asset A of the liquidity pool + Asset assetB; // asset B of the liquidity pool + int64 amountA; // current amount of asset A in the pool + int64 amountB; // current amount of asset B in the pool + int64 stakes; // total number of pool shares owned by the account + + // reserved for future use + union switch (int v) + { + case 0: + void; + } + ext; +}; + +/* Represents information about the account stake in the pool */ +struct LiquidityStakeEntry +{ + AccountID accountID; // account this liquidity stake belongs to + uint32 poolID; // pool invariant identifier + Asset assetA; // asset A of the liquidity pool + Asset assetB; // asset B of the liquidity pool + int64 stake; // share of the pool that belongs to the account + + // reserved for future use + union switch (int v) + { + case 0: + void; + } + ext; +}; + @@ -431,6 +468,10 @@ struct LedgerEntry DataEntry data; case CLAIMABLE_BALANCE: ClaimableBalanceEntry claimableBalance; + case LIQUIDITY_POOL: + LiquidityPoolEntry LiquidityPool; + case LIQUIDITY_STAKE: + LiquidityStakeEntry LiquidityStake; } data; @@ -479,6 +520,29 @@ case CLAIMABLE_BALANCE: { ClaimableBalanceID balanceID; } claimableBalance; + +case CLAIMABLE_BALANCE: + struct + { + ClaimableBalanceID balanceID; + } claimableBalance; + +case LIQUIDITY_POOL: + struct + { + uint32 poolID; + Asset assetA; + Asset assetB; + } LiquidityPool; + +case LIQUIDITY_STAKE: + struct + { + uint32 poolID; + AccountID accountID; + Asset assetA; + Asset assetB; + } LiquidityStake; };
--- a/src/xdr/Stellar-transaction.x +++ b/src/xdr/Stellar-transaction.x @@ -48,7 +48,9 @@ enum OperationType END_SPONSORING_FUTURE_RESERVES = 17, REVOKE_SPONSORSHIP = 18, CLAWBACK = 19, - CLAWBACK_CLAIMABLE_BALANCE = 20 + CLAWBACK_CLAIMABLE_BALANCE = 20, + DEPOSIT_POOL_LIQUIDITY = 21, + WITHDRAW_POOL_LIQUIDITY = 22 }; @@ -390,6 +392,38 @@ +/* Deposits funds to the liquidity pool + + Threshold: med + + Result: DepositPoolLiquidityResult +*/ +struct DepositPoolLiquidityOp +{ + uint32 poolID; // pool invariant identifier + Asset assetA; // asset A of the liquidity pool + Asset assetB; // asset B of the liquidity pool + int64 maxAmountA; // maximum amount of asset A a user willing to deposit + int64 maxAmountB; // maximum amount of asset B a user willing to deposit +}; + +/* Withdraws all funds that belong to the account from the liquidity pool + + Threshold: med + + Result: WithdrawPoolLiquidityResult +*/ +struct WithdrawPoolLiquidityOp +{ + uint32 poolID; // pool invariant identifier + Asset assetA; // asset A of the liquidity pool + Asset assetB; // asset B of the liquidity pool +}; + @@ -1186,6 +1220,67 @@ +/******* DepositPoolLiquidity Result ********/ + +enum DepositPoolLiquidityResultCode +{ + // codes considered as "success" for the operation + DEPOSIT_SUCCESS = 0, + // codes considered as "failure" for the operation + DEPOSIT_MALFORMED = -1, // bad input + DEPOSIT_NO_ISSUER = -2, // could not find the issuer of one of the assets + DEPOSIT_POOL_NOT_ALLOWED = -3, // invalid pool assets combination + DEPOSIT_INSUFFICIENT_AMOUNT = -4, // not enough funds for a deposit + DEPOSIT_ALREADY_EXISTS = -5, // account has a stake in the pool already + DEPOSIT_LOW_RESERVE = -6 // not enough funds +}; + +struct DepositPoolLiquiditySuccessResult +{ + // liquidity pool stake that has been created + LiquidityStakeEntry stake; +}; + +union DepositPoolLiquidityResult switch ( + DepositPoolLiquidityResultCode code) +{ +case DEPOSIT_SUCCESS: + DepositPoolLiquiditySuccessResult success; +default: + void; +}; + +/******* WithdrawPoolLiquidity Result ********/ + +enum WithdrawPoolLiquidityResultCode +{ + // codes considered as "success" for the operation + WITHDRAW_STAKE_SUCCESS = 0, + // codes considered as "failure" for the operation + WITHDRAW_STAKE_MALFORMED = -1, // bad input + WITHDRAW_STAKE_NOT_FOUND = -2, // account doesn't have a stake in the pool + WITHDRAW_STAKE_NO_TRUSTLINE = -3, // account does not have an established and authorized trustline for one of the assets + WITHDRAW_STAKE_TOO_EARLY = -4 // an attempt to withdraw funds beforethe lockup period ends +}; + +struct WithdrawPoolLiquiditySuccessResult +{ + int32 poolID; // pool invariant identifier + Asset assetA; // asset A of the liquidity pool + Asset assetB; // asset B of the liquidity pool + int64 amountA; // amount of asset A withdrawn from the pool + int64 amountB; // amount of asset B withdrawn from the pool + int64 stake; // pool share that has been redeemed +}; + +union WithdrawPoolLiquidityResult switch ( + WithdrawPoolLiquidityResultCode code) +{ +case WITHDRAW_STAKE_SUCCESS: + WithdrawPoolLiquiditySuccessResult success; +default: + void; +}; +
--- a/src/xdr/Stellar-ledger.x +++ b/src/xdr/Stellar-ledger.x @@ -84,6 +84,8 @@ struct LedgerHeader { case 0: void; + case 1: + uint32 liquidtyPoolYield; // fee charged by liquidity pools on each trade in permile (‰) } ext; };
Modified semantics of trading-related operations presented in this CAP allows to
drastically reduce the number of new interaction flows. Liquidity from the pools
will be immediately available for existing Stellar applications through the
convenient offers and path payment interface operations.
In this section, a constant product invariant (x*y=k
) is used for all
calculations. Other invariants can be implemented as separate pools with
different price quotation formulas and execution conditions.
DepositPoolLiquidityOp
operation transfers user funds to the selected
liquidity pool defined as LiquidityPoolEntry
.
assetA
andassetB
is performed. If an asset is not a native asset and the issuerDEPOSIT_NO_ISSUER
error is returned.assetA
=assetB
should result inDEPOSIT_POOL_NOT_ALLOWED
error. This version of the proposal doesn't implyLiquidityStakeEntry
by operation sourcepoolID
, assetA
, and assetB
. If correspondingLiquidityStakeEntry
was found, DEPOSIT_ALREADY_EXISTS
error is returned.assetA
, assetB
. If any of theDEPOSIT_INSUFFICIENT_AMOUNT
error returned.maxAmountA
and maxAmountB
. The current price can be determinedP=Ap/Bp
where Ap
and Bp
- correspondingly amount of token A and tokenBdm=Ad/P
and Adm=Bd*P
whereAd=min(maxAmountA,accountBalanceA)
, Bd=min(maxAmountB,accountBalanceB)
,Bdm
and Adm
– maximum effective amounts of tokens A and B that can beDEPOSIT_INSUFFICIENT_AMOUNT
error returned. In case if maxAmountA
ormaxAmountB
provided in the operation equals zero, the node takes the valueS=A*B*Sp/(Ap*Bp)
where S
- share of theA
and B
- actual amount ofSp
- total stakes currently in the pool (the value fromLiquidityPoolEntry
), Ap
and Bp
- correspondingly amount of token AS
=0 (this can be the case with aDEPOSIT_INSUFFICIENT_AMOUNT
. If the native asset balance does not satisfyDEPOSIT_LOW_RESERVE
error returned.LiquidityPoolEntry
does not exist on-chain (this is the first deposit) itS=min(A,B)
.LiquidityStakeEntry
with stake
=S
.numsubEntries
for the source account incremented.LiquidityPoolEntry
setting amountA
+=A
, amountB
+=B
,stakes
+=S
.DEPOSIT_SUCCESS
code returned.WithdrawPoolLiquidityOp
operation withdraws funds from a liquidity pool
proportionally to the account stake size.
LiquidityStakeEntry
by operation sourcepoolID
, assetA
, and assetB
. IfLiquidityStakeEntry
was not found,WITHDRAW_STAKE_NOT_FOUND
error is returned.Kw=S*Ap*Bp/Sp
where Kw
-S
- share of the account inLiquidityStakeEntry
, Sp
- total number of pool sharesLiquidityPoolEntry
, Ap
and Bp
- current token amount of asset AP=Ap/Bp
.A=√(Kw*P)=Ap√(S/Sp)
B=√(Kw/P)=Bp√(S/Sp)
LiquidityStakeEntry
has been created after now() - 24hours
,WITHDRAW_STAKE_TOO_EARLY
error returned.assetA
and assetB
. If the source account doesWITHDRAW_STAKE_NO_TRUSTLINE
error returned.numSubEntries
for the source account decremented.LiquidityPoolEntry
updated: amountA
-=A
, amountB
-=B
, stakes
-=S
.LiquidityStakeEntry
removed.WITHDRAW_STAKE_SUCCESS
code returned.To deal with disambiguation and simplify the aggregation process, assetA
and
assetB
in LiquidityPoolEntry
and LiquidityStakeEntry
should always be
sorted in alphabetical order upon insertion. The comparator function takes into
account the asset type, asset code, and asset issuer address respectively.
Ledger header contains new fields that can be adjusted by validators during the
voting process.
liquidtyPoolYield
represents the fee charged by a liquidity pool on each trade
in permile (‰), so the poolFeeCharged=tradeAmount*liquidtyPoolYield/1000
Behavior updated for ManageSellOfferOp
, ManageBuyOfferOp
,
CreatePassiveSellOfferOp
, PathPaymentStrictReceiveOp
,
PathPaymentStrictSendOp
operations.
When a new (taker) order arrives, the DEX engine loads the current state of all
liquidity pools for the traded asset pair, fetches available cross orders
(maker orders) from the orderbook, and iterates through the fetched orders.
On every step, it checks whether the next maker order crosses the price of the
taker order. Before maker order execution the engine estimates the number of
tokens that can be traded on each liquidity pool for the same trading pair up to
the price of the current maker order.
The maximum amount of tokens A to be bought from the pool can be expressed as
Aq=Ap-Bp*(1+F)/P
where F
- trading pool fee, P
- maximum price (equals
currently processed maker order price in this case), Ap
and Bp
- current
amounts of *asset A and asset B in the pool respectively.
If Aq>0
, the corresponding amount of tokens is deducted from the pool and
added to the variable accumulating the total amount traded on the pool.
After that, the current order itself is matched to the remaining taker order
amount, and so on, up to the point when a taker order is executed in full. If
the outstanding amount can't be executed on the orderbook nor the pool, a new
order with the remaining amount is created on the orderbook.
In the end pool settlement occurs – traded asset A tokens are deducted from
the pool and added to the account balance, a matching amount of asset B
transferred from the account balance to the pool.
A trade against a pool generates a ClaimOfferAtom
result with sellerID
and
offerID
equal to zero.
Basic liquidity pools implementation separately from the existing DEX has
several shortcomings:
ManageSellOfferOp
and ManageBuyOfferOp
, it also requiresSwapSellOp
and SwapBuyOp
for the interaction with AMM.SwapPathPaymentStrictReceiveOp
and SwapPathPaymentStrictSendOp
.Advantages of the proposed approach:
DepositPoolLiquidityOp
and WithdrawPoolLiquidityOp
operations have
intentionally simplified interfaces. WithdrawPoolLiquidityOp
removes all
liquidity from the given pool supplied by the account. This makes partial funds
withdrawal somewhat difficult because a client will need to remove all the
liquidity and then call the DepositPoolLiquidityOp
to return the desired
portion back to the pool. But this makes the overall process much more reliable,
especially under the price fluctuation conditions. Both operations can be
executed in the same transaction, so overall it looks like the best option.
Proposed enforcement of the minimal pool deposit retention time results in a
more predictable liquidity allocation and helps to avoid frequent switching
between pools from users willing to maximize profits by providing liquidity only
to the most active markets.
The poolID
identifier present in new operations and ledger entries provides
the way to use more than one pool per trading pair, with different price
quotation functions and execution parameters.
Proposed changes do not affect existing ledger entries. Updated trading
operations semantics are consistent with the current implementation and do not
require any updates on the client side.
This CAP should not cause a breaking change in existing implementations.
The price quotation on the liquidity pool adds additional CPU workload in the
trading process. However, this may be compensated by the fewer orderbook orders
matched.
Utilized constant product formula may require 128-bit number computations which
may be significantly slower than 64-bit arithmetics. This potentially can be
addressed by modifying ledger entries to store values derived from asset
balances in the pool (L=√(A*B)
and √P=√(B/A)
).
Every LiquidityPoolEntry
requires storage space on the ledger but unlike
LiquidityStakeEntry
it is not backed by the account base reserve.
TBD
--
You received this message because you are subscribed to the Google Groups "Stellar Developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email to stellar-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CAJxYNoa2k-cpgTGjQVSGDb8hON1qgzQMqswaZ6CXyOjbUaU0kw%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/1e57d76b-77df-fdf7-f7be-859c31d131ed%40coinqvest.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CAHzj05UG911Pq0BjszYXP3mzOGTFbsXcxsUzTMZHKbfhc4xR7w%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CABf4b3y0h%2B1ZVe9Yd1oskNq4XL0HaYFMrujZstxKixj7STaUVA%40mail.gmail.com.
LiquidityStakeEntry - maybe we don’t need the assetA, assetB fields here since it’s specified on the poolID? At least we shouldn’t save this on-chain (db) since we can find this out by a join and don’t need to duplicate asset information for every LiquidityStakeEntry.
WithdrawPoolLiquidityOp - maybe we can specify only units of shares we want to withdraw? Or specify as a % instead of always withdraw 100%? Users may not want to withdraw 100% always because that is a taxable event.
DepositPoolLiquidityOp - Is it possible to use this operation to add more to our already staked amount? (i.e. add more liquidity)
- rename: DEPOSIT_NO_ISSUER to DEPOSIT_INVALID_ASSET_A, DEPOSIT_INVALID_ASSET_B, DEPOSIT_INVALID_ASSET_AB. This covers the case where it has an issuer but code / combination of code:issuer is invalid. By separating out A and B we also cover the case where we can identify which asset was invalid or if both were invalid. If not required then we can just generalize to DEPOSIT_INVALID_ASSET instead of DEPOSIT_NO_ISSUER.
Deposit success result - also include oldStake in the result?
WITHDRAW_STAKE_NO_TRUSTLINE - would it be a good idea to make it invalid (fail) to remove a trustline if you have a stake in a pool for that asset?
add: NOT_ENOUGH_TRUST - it’s possible that the user will try to withdraw tokens but the withdrawal will result in the user’s account holdings more tokens than what they trust. This should be a failure scenario since trustlines have an upper limit.
Let's assume BTC/USD = $50,000, the user should deposit say $50,000 and 1 BTC so there is equal amounts of "value" of each token deposited. However, if the user deposits 0.1 BTC and $50,000 (incorrect ratio) then they have deposited more value in USD than they did in BTC.
Not sure if I'm thinking about this correctly, but can it be this? A=Ap * (S/Sp) B=Bp * (S/Sp) (i.e. if I own 10% of the staked tokens then give me 10% of Ap and Bp each)
Withdraw stake too early -- if we were to allow users to add more deposits after their initial deposits, maybe we can add a lastDepositLedger field on the LiquidityStakeEntry and lock the withdrawals of the entire amount for 24 hours. This seems to be the simplest solution that allows follow-on deposits without getting into situations where we have to keep track of how much was deposited at what time so we unlock amounts correctly.
Maximum amount of tokens of assetA that can be bought from the pool - this section describes how many tokens of Aq we can take from the pool before we hit the first maker order -- what is the calculation for what price we will use?
Aq=Ap-Bp*(1+F)/P
where F
-
trading pool fee, P
- maximum price
(equals currently processed maker order price in this case), Ap
and Bp
- current amounts of asset A and asset B in the pool respectively. I think the invariant in this situation should be that if the users buys and sells the same small delta amount from the pool one after the other then the user should end up with less money than they started with (because that is consumed by the fee of the pool)
SwapSellOp and SwapBuyOp - What happens when a user wants to swap an amount that would have crossed the first maker order on the DEX if they had used a ManageOffer?
Ideally, stellar-core would factor in the maker order on the DEX to give them a better price than if they just consumed from the AMM. This sounds very similar to the updated implementation for the ManagerOffer ops -- the only difference being that there is no price associated with the operation and it can't stand on the books if partially filled, i.e. it is a market order.
Multiple pools -- as a v1 I think it will be easier and more manageable to implement only 1 pool per trading pair so liquidity is not fragmented.
LiquidityPoolEntry reserve -- can we charge a reserve against the account that creates the pool?
Alternatively, since pools are not owned by anybody (and maybe cannot be deleted also), we could have the "reserve" amount burned from the account that creates the pool? -- or moved to a locked account.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CANK%2BCd4nmGdbzuboxO0wGEAG%2BwPmQ%3DQmKwNv5c2c54WWu6k0Ew%40mail.gmail.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CAJxYNoYgH1pno52zQAShdesafDXd4OtsEu%2Bvh1wGYb8e5ArEhg%40mail.gmail.com.
Status Update
Following the feedback collected from the mailing list, questions list, and working group discussion, I prepared a revised version of the CAP draft (it's here: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0037.md).
Changes:
I made a simple js calculator to speed up AMM logic testing: https://amm-calculator.pages.dev/ It is extendable, so once we decide to add any new invariant or change the deposit/withdrawal logic, it could be tested against the current implementation in no time.
Also, I'd like to address a few questions published here.
UniswapV3-like pool
Although I see a huge potential in this idea, there are several arguments against it. I'll try to compile the feedback received from the mailing list, my personal p2p conversations, and working group consultations.
It's important to consider that this LP ecosystem is fundamentally different than the Ethereum LP ecosystem as liquidity providers are competing for spread with market makers. It is likely that market makers will create their orders inside the spread created by liquidity pool fees. As such, liquidity pools will probably end up "picking up the slack" by filling orders when market makers fail to adjust their spread correctly. That being said, this may not be the case initially as DEX spreads are currently very large.
If the liquidity pool fee is small enough (for example, Uniswap v2 features 0.3% swap fee), it's very unlikely that market makers will be able to take advantage by maintaining such a tight spread without exposing themselves to additional risk.
How is the yield/liquidity pool fee set? I'd imagine it would make the most sense to allow users to set it in the Deposit Liquidity Pool Op. Users are going to need a good amount of flexibility in setting their pool fees as the economic viability of different fee levels will vary based on the liquidity of asset pairs.
Simultaneous trading against multiple pools with different fee parameters is tricky from a technical point of view. Therefore, my proposal describes the universal network-wide fee determined by validators voting (the liquidtyPoolYield parameter).
I don't think we need to be concerned about having many separate liquidity pools at all. Since they're all using the DEX, the liquidity will be aggregated.
Trading against multiple pools is much more expensive in terms of resources (by orders of magnitude). It's the primary concern here.
Can we add some references to what AMM is and give credit to existing implementations? The section which describes the interaction between SDEX and AMM and how matching works could benefit from a simplified summary.
My intention was to make the CAP as brief as possible since the proper description of all AMM concepts and mechanics might be quite lengthy. The impermanent loss section itself will be as large as the entire CAP draft. I'd be happy to include references to AMM whitepapers, but the CAP format currently doesn't imply citation. My introductory blogpost contains more comprehensive AMM theory analysis and ordebrook+amm trading examples. Not sure whether we need an exhaustive AMM explanation in the CAP doc or not. The interleaved execution process you described is very precise. I extended the section containing the trading process logic, but I'll definitely add more details if some aspects are still unclear.
-------
Thanks for the feedback!
Please make sure to check the revised CAP draft and the alternative proposal by Jon Jove.
uint32 poolType
This should really be its own type `PoolType` instead of using `uint32`
Something like
```
enum PoolType {
POOLTYPE_CONSTANT_PRODUCT_v0 = 0
}
```
That way it's extensible (possibly as something else than a uint32), is validated everywhere, and it's clear which values are allowed.
+/* Represents
information about the account stake in the pool */
+struct LiquidityStakeEntry
Nit: "in a pool"
+ uint32 liquidtyPoolYield; // fee charged by liquidity pools on each trade in permile (‰)
Nit: typo ` liquidityPoolYield`
DepositPoolLiquidityOp:
Validity:
"This version of the proposal doesn't imply any other restrictions, but this may change in the future."
I imagine that you would check if the pool type is valid?
In general: you should use the names from the xdr, it makes it harder to follow the logic.
I think you probably want to reorder certain things as to avoid mutating variables (as to avoid defining "a" as "maxAmountA" only to adjust later), this will also make it clearer which conditions are really needed and when they apply.
I would get rid of the complexity tied to balance requirements etc: I think you can just model what you're looking for as a sponsorship of the `LiquidityPoolEntry ` and not special case (from a pool point of view) the account that created it.
Formula for stake
calculation: I think you're implying that all the math is done in floating
point (sqrt and floor)?
This should be specified better: I imagine you're looking at IEEE754, specifically binary64 (56 bit precision), what about rounding mode when performing operations in the floating point space (there are 5)? See IEEE 754 - Wikipedia
I don't see any guards against overflows when you perform operations in int64 space.
I don't see how authorization is handled.
Use of `H(operation source account address | poolType | assetA | assetB)` as LedgerKey for LiquidityStakeEntry:
I don't see why you need to do this: the ledger key can just be `(accountID, poolType, assetA, assetB)`.
I see at the end that this is to "optimize storage", but we have to store all those fields anyways, so it's actually additional storage.
WithdrawPoolLiquidityOp
Validity needs to be defined.
I think it would be better to have authorization issues get their own error code.
Is it possible to reach LiquidityPoolEntry.stakes=0 at the end? If so do you expect the pool to be destroyed (I think you do?)?
LedgerHeader changes
If you make `liquidtyPoolYield` mutable doesn't that potentially create problems like what was identified in https://github.com/98farhan94/doc/blob/main/Curve_Vulnerability_Report.pdf ?
Trading update
I understand the math you're trying to do when trading using multiple steps, but some explanation on which property you're trying to get would help. In particular, I would like to see it articulated in such a way that if we have more than constant product and/or more than one pool we can reason about it (as this property is what we will carry forward).
This "step" algorithm should probably be adjusted: right now it's actually pretty hard to see which of the bullet points applies when.
Related (on the how hard it's going to be to validate) there are a bunch of subtleties in the existing DEX. In particular smaller trades may not work because of the way rounding works, so this needs to be specified somehow as I think that could cause the order books to be slightly crossed (the AMM pool does not have this problem) or you'll have to allow a "step" to skip the DEX in some situations. Maybe this is fine (but this goes back to which property you're aiming for exactly).
I noticed later that some of this was covered in the "rationale" section - I would still like to see which property and invariants are desired in this section.
Also, I would like to hear from people in the ecosystem about this particular strategy (that will require implementing the same logic in Horizon to give a quote on any given asset pair). At a minimum this would have to be implemented in Horizon.
Missing semantic changes
Authorization and clawback
What happens if a stake holder gets its trustline revoked?
How is clawback going to work (I imagine that this is the same answer than in the other thread for CAP38).
Rationale
I see that under "advantages" you're calling out the strategy as "best price" and "always balanced" - I think this may not be true due to rounding in the existing DEX that has to alternate which side (maker or taker) benefits from rounding.
"smaller attack surface due to not having direct pool access" - I am not sure I understand what you mean? Clearly if there are no offers in some price range all interactions will be directly against a given pool.
--
This is in response to OrbitLens' blog post (https://stellar.expert/blog/stellar-amms-at-crossroads-between-triumph-and-disaster).
Most of this is going to follow the order of OrbitLens' blog post, but I want to open with an acknowledgement that the optimal interleaved execution is not NP-complete. I have reviewed OrbitLens' approach to solving the optimization problem (https://groups.google.com/g/stellar-dev/c/Ofb2KXwzva0/m/YVBKq-3PDAAJ) and I believe his method does work. In the case of multiple constant product pools, it actually works very elegantly. There may be some challenges with rounding, as he acknowledges, but that is a relatively minor detail. *I will update CAP-38 to reflect the fact that this problem is easier than it appeared.*
## Multi-Pool World by Example
OrbitLens constructed a set of scenarios involving multiple liquidity pools to demonstrate a difference between CAP-37 and CAP-38. I want to emphasize that *both CAP-37 and CAP-38 only support a single liquidity pool* using the constant product invariant with a fee of 0.3%. But let's consider the cases anyway:
### Case 1
Under these circumstances, it would be impractical to purchase 35k USDC even if all the liquidity were in a single no-fee constant product pool with total reserves 60k USDC, 120k XLM. It would cost 168k XLM, for an effective purchase price of 0.208 USD/XLM compared to a market price of 0.5 USD/XLM. I doubt anyone would be happy with this situation, so most operations would fail due to bounds on the trade price. In other words, this *trade is simply too big for the amount of liquidity available* regardless of the algorithm used to execute it.
### Case 2
I haven't checked the math, but it is an accurate representation that you will get a better price with CAP-37.
### Case 3
This is a significant misrepresentation. The blog post suggests that the four trades would have to be submitted separately, thereby sacrificing atomicity. This is, of course, false because Stellar supports atomic transactions (https://developers.stellar.org/docs/glossary/transactions/). So you don't have to worry about getting interrupted by arbitrageurs, because the four trades can be bundled into a single transaction.
What about the arbitrageurs? The reality is that *the arbitrage bots will awaken regardless of whether CAP-37 or CAP-38 is used*. It isn't hard to see why: arbitrageurs will simply arbitrage the liquidity pool against the centralized exchange. This is probably more profitable than doing an on-chain arbitrage between liquidity pools and the order book, because there will be less slippage on the centralized exchange, so this would be the preferred method for any serious arbitrageur. For comparison, executing a $5000 trade for XLM/USD on Coinbase would typically have about 5-10 basis points of slippage.
### Case 4
As noted above, CAP-38 does not support multiple liquidity pools for a single asset pair. There is no mechanism to deprecate a pool or make the pool withdraw only. The basic extension points for multiple liquidity pools are there in acknowledgment of the fact that we are not clairvoyant predictors of the future, and there may be a time when having a different type of liquidity pool outweighs the disadvantages that OrbitLens listed (and I agree about those disadvantages wholeheartedly).
### Case 5
This is an inaccurate portrayal of the routing behavior of CAP-38. CAP-38 attempts to execute on the liquidity pool and the order book, and actually executes whichever way provides the best price. If a malicious actor attempted to grossly increase the slippage in the pool by withdrawing their reserves then that pool would no longer have the best price. Therefore, the exchange will not be performed in the pool and the user will not experience any change in price. In this context, the attack has no impact at all.
## Closer look at the Arbitrage Problem
OrbitLens provides a reasonable description of the behavior of arbitrage bots, except that his analysis of the fees is incorrect. The fee auction is a variant on an all-pay auction (https://en.wikipedia.org/wiki/All-pay_auction) in which every transaction is charged the fee of the last included transaction. In the context of large arbitrage opportunities, more arbitrage transactions will be submitted than the ledger can process so the last included transaction is likely to be an arbitrage transaction. There is no advantage to having duplicate arbitrage operations in the same transaction, so each transaction would be expected to have a single operation. With current network settings of 1000 operations per ledger, there is a 1/1000 chance of winning the opportunity. The expected value of submitting an arbitrage transaction is (value of the arbitrage opportunity) / 1000. Therefore, the example of an 800 XLM arbitrage opportunity would lead to fee bids of about 0.8 XLM. This is high compared to the base fee, but approximately 3 orders of magnitude lower than OrbitLens' analysis suggests.
## Fundamental Misconceptions
OrbitLens discusses a variety of misconceptions, and the analysis is quite interesting. I already acknowledged the error in my suggestion that optimal interleaved execution is NP-complete. I will discuss some of the remaining sections.
### "Interleaved execution is a premature optimization, and can be done later"
The blog post suggests that one of the proposals introduces operations to trade directly against individual pools while the other does not. This is false, as both proposals use existing operations to trade against liquidity pools and both proposals have on-chain routing (CAP-37 splits among venues, CAP-38 chooses the one best venue). As a consequence, *there would be no backwards compatibility issues to implement interleaved execution later for CAP-38*.
Orbit also points out the reality that it can take months to years to solidify a CAP. But if it would only take "several additional weeks" to support interleaved execution today--which I think is extremely optimistic--then surely a new CAP with no backwards compatibility issues for that could be completed in approximately the same amount of time.
### Problems of interleaved execution across several pools
As noted above, OrbitLens' method does work. One residual challenge is that not all functions can be inverted analytically. Therefore even if it is possible to compute a "price bound" formula for a specific liquidity pool, multiple pools might require an iterative solver which would increase the cost. If there is no "price bound" formula for a specific liquidity pool (I am unsure if this formula exists for the StableSwap invariant, but it might) then I am not sure if this approach is practical in general.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/56d0b86d-4cca-43e0-842f-bd921b6f2787n%40googlegroups.com.
Right at the beginning of my recent blog post, I mentioned that the root of the problem resides in the general strategy adopted to handle fractured on-chain liquidity. CAP38 opens Pandora's box of possible arbitrage between the orderbook and liquidity pool (which is dangerous by itself), but the future implications of having a dozen of pools for the same asset make it especially scary. In the video stream, you presented a case of having immutable pool parameters, so any future protocol upgrade that changes pool-related logic must leave the existing pool params (fees and everything else) untouched, naturally resulting in "legacy pools" residing on-chain. Independent Uniswap v1, v2, and v3 pools show the example of such non-breaking contract upgrades when a decent portion of liquidity remains in old contracts. Giving the suggestion of having several fee tiers implemented as individual pools makes the perspective of a dozen similar pools per asset pair in the future not so exaggerated anymore.
I believe that the case of independent execution on a liquidity pool/orderbook has been extensively covered in my first blogpost, so this time I primarily focused on the multi-pool scenario that has been actively discussed by CAP committee on the livestream.
> I want to emphasize that *both CAP-37 and CAP-38 only support a single liquidity pool* using the constant product invariant with a fee of 0.3%
CAP37 proposes a flexible mechanism of the collective pool fee rate voting, focusing on reducing the number of pools per asset pair. Maybe I got a wrong impression from the discussion, but people are actively looking for ways to have adjustable fees for CAP38 as a uniform 0.3% doesn't seem like a convenient one-size-fits-all constant. And the variant of having several identical pools with either adjustable or pre-defined yield rates has been extensively debated by the committee.
> Under these circumstances, it would be impractical to purchase 35k USDC even if all the liquidity were in a single no-fee constant product pool with total reserves 60k USDC, 120k XLM. It would cost 168k XLM, for an effective purchase price of 0.208 USD/XLM compared to a market price of 0.5 USD/XLM.
First of all, in this example, I intentionally excluded the orderbook from the calculation to avoid speculations about orders placement and resulting price. However, from the real-world usage, we know that the liquidity profile in the liquid orderbook resembles normal distribution, so we can safely assume that including an orderbook in the equation will likely improve the average execution price to somewhere closer to 0.3-0.4 USDC/XLM.
> I doubt anyone would be happy with this situation, so most operations would fail due to bounds on the trade price.
While it's obviously an edge-case event, there is a multitude of situations that may require order execution at any price.
For instance, collateral position liquidation, derivatives expiration, anchored token price drop induced by force majeure circumstances.
> The blog post suggests that the four trades would have to be submitted separately, thereby sacrificing atomicity. This is, of course, false because Stellar supports atomic transactions (https://developers.stellar.org/docs/glossary/transactions/). So you don't have to worry about getting interrupted by arbitrageurs, because the four trades can be bundled into a single transaction.
To my knowledge, neither of the existing trading interfaces allows bundling several path payments into a single transaction. Not even saying that the average user will ever think about doing this. Your suggestion implies that all wallets should update their trading interfaces and employ an elaborate price estimation algorithm to split a single swap into several path payment operations.
So we get a much more extensive ledger capacity utilization, larger transaction fees, and increased validators load. As the transaction is atomic, the likelihood of the entire trade failing grows proportionally to the number of included path payments since the market state in the multiuser environment is nondeterministic by its nature – significant price shift on a single pool will prevent trades against all other pools from execution. This can be partially addressed by specifying higher slippage tolerance in the path payments, although such a decision consequently exposes a user to greater risk.
> The reality is that *the arbitrage bots will awaken regardless of whether CAP-37 or CAP-38 is used*. It isn't hard to see why: arbitrageurs will simply arbitrage the liquidity pool against the centralized exchange. This is probably more profitable than doing an on-chain arbitrage between liquidity pools and the order book, because there will be less slippage on the centralized exchange, so this would be the preferred method for any serious arbitrageur.
While inter-platform arbitrage looks like a very simple and attractive thing, anyone who ever done the arbitrage between exchanges, would tell that there are plenty of risks here:
For example, check anchored BTC price history and Binance chart. It clearly shows that arbitrage is not super efficient.
On the contrary, executing arbitrage on-chain is almost immediate, virtually free, and doesn't carry any slippage or volatility risk. That's why it's a golden opportunity for arbitragers that will compete with each other, flooding the network and driving it unusable. From the perspective of the on-chain arbitrage bot, any $0.1 profit chance is worth it. Therefore the comparison of these two arbitrage types is clearly inaccurate. Not saying that only a limited number of tokens are traded elsewhere except Stellar DEX at the moment.
> Case 5. This is an inaccurate portrayal of the routing behavior of CAP-38.
As I pointed above, this blog post has been primarily focused on the future interoperability between several pools in the context of different proposed mechanics. I removed this paragraph from the post. Admittedly, it doesn't fit the CAP37 vs CAP38 topic. Sorry for the misleading statement, I should have stay focused on the primary subject.
> The expected value of submitting an arbitrage transaction is (value of the arbitrage opportunity) / 1000. Therefore, the example of an 800 XLM arbitrage opportunity would lead to fee bids of about 0.8 XLM.
Actually, my example implied a slightly lower fee. 750 XLM in fees spent for all transactions -> 0.75 XLM. Maybe I should have chosen better wording. My primary point here is that the maximum network fee is directly proportional to the emerging arbitrage opportunity. With 10,000XLM worth arbitrage trade, the network fee may reach up to 10XLM per operation.
> *there would be no backwards compatibility issues to implement interleaved execution later for CAP-38*
After thinking about this for a bit, I agree with you. With the CAP38 in its current state, there will be no backward incompatibilities for the interleaved execution implementation.
> Orbit also points out the reality that it can take months to years to solidify a CAP. But if it would only take "several additional weeks" to support interleaved execution today--which I think is extremely optimistic--then surely a new CAP with no backwards compatibility issues for that could be completed in approximately the same amount of time.
From the top of my head, I can't remember any CAP that has been drafted, discussed, finalized, implemented, and shipped to the network in under half a year term, except urgent bug fixes.
> As noted above, OrbitLens' method does work. One residual challenge is that not all functions can be inverted analytically.
Obviously, I can't claim that any invariant can be inverted analytically. Yet, I have a rather strong conviction that if one can use a formula to estimate a swapped amount and the number of tokens in the pool after the swap, it can be altered to estimate the price after the trade. Again, this might not work with oracle-based AMMs or other algorithms that rely on external information, although this is a very broad question by itself.
---
Thanks for taking time to review my post, Jon.