+struct
ClaimLiquidityAtom
+{
+ LiquidityPoolKey pool;
+ int64 amountSold; // amount taken
from the pool
+ int64 amountBought; // amount and
asset sent to the pool
+};
How do we know which asset was sold vs taken?
Actually, maybe it would be cleaner/easier for SDKs to have the `AccountID` be the variable as what we're trying to pivot on is where the liquidity is coming from?
```
union LiquiditySource switch(LiquidityType type)
{
case LIQUIDITY_TYPE_OFFERV0:
struct
{
uint256 sellerIDEd25519; // Account that owns the offer
int64 offerID;
} offerV0;
case LIQUIDITY_TYPE_OFFER:
struct
{
AccountID sellerID; // Account that owns the offer
int64 offerID;
} offer;
case LIQUIDITY_TYPE_POOL:
LiquidityPoolKey pool;
};
struct ClaimAtom
{
// emitted to identify the offer
LiquiditySource source;
// amount and asset taken from the source
Asset assetSold;
int64 amountSold;
// amount and asset sent to the source
Asset assetBought;
int64 amountBought;
};
```
Use of `LiquidityPoolKey`:
In the future I imagine that we'll have
I don't know how many parameters we'll need to identify a pool, but this may get fairly big: there will be the base fee, potentially other things that restrict amounts, time etc.
It seems to me that we should try to make it more efficient by default.
If we assume that pools tend to be "long lived", we can take advantage of it: if we had an explicit "createpool" (that is idempotent) that creates a pool with the full `LiquidityPoolKey`, we can then use a "liquidity pool identifier" everywhere else something like H(LiquidityPoolKey).
If we do that, we could add right away a fee (expressed in basis points for example) in the `LiquidityPoolKey` (which saves us from having to introduce a new complete type just for that later on) - we would only allow to set it to a hard coded constant in the first version (and open it up to more values later on).
LiquidityPoolDepositOp
Validity:
You're referencing `maxAmountB` but there is only `minAmountB`
Apply:
"A" or "B" can be NATIVE (no trustline)
You should move the UNDERFUNDED check all the way to the top, that way you'll also check for that condition when creating the pool.
Can computations like `amountB = reserveB * amountA / reserveA` overflow int64?
I think the answer is yes, we could error out in this situation, or maybe a better way to deal with this a bit differently: in this situation we can reduce amounts, but this breaks the contract you were going for (where you rely on the fact that you always fix one amount, and use minimum for the other one). Maybe using what Orbitlens did (use a max error on price) allows to fix this?
Use of "sqrt" - I asked the same thing in CAP37 - I don't know if we can guarantee the result using standard libraries. Also if this is using "floating point" like double, is the loss of precision (56 bits mantissa) a problem?
LiquidityPoolWithdrawOp
Apply:
Same issue than for deposit wrt NATIVE support
Is it possible to have amountA or amountB == 0? I suspect yes, we should error out in that case?
ChangeTrustOp
Should specify limit (max int 64).
PathPaymentStrictSendOp and PathPaymentStrictReceiveOp
I like this interaction model as it's trivial to validate.
There is one small gap:
You need to define a strict ordering. Right now it's not specified which liquidity provider wins in case of a tie.
I'd say that order book is better in case of a tie?
For pools, we can leave this for later (as there is only one per asset pair), intuitively I'd say it should be the one that causes the smallest shift in price (favoring larger pools) as to minimize potential arbitrage.
I like this idea of having the pool ref counted using trustlines. I am fine with no additional reserve requirement in this case (as opposed to using sponsorship).
As to make invariant checking easier, should we add the count to the pool?
Clawback - we need something there, the proposal doesn't address it (but I saw that Siddharth is working on it)
What happens when an account gets its auth revoked?
Right now it looks like nothing happens (funds stay in the pool), but the account cannot withdraw from the pool.
From a framework point of view, this makes it behave like a "BalanceEntry". This may not be enough. In the case of offers, we cancel offers in this situation. What may make more sense is to figure out how to force a withdrawal in this situation. This can then be combined with the normal clawback flows (if available).
--
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/CAG4FKhxzGFPPYVsQe5GdrRnMBhb6w%2B2ym4ObvKbzVHb0%3DWRNrA%40mail.gmail.com.
ChangeTrustOp
Should specify limit (max int 64). ---> no need for this (we can let people specify the limit they want)
Rationale/XDR
I misread the rationale on "unused liquidity pools" (call it brainfart).
I reviewed the semantics like you intended (using pool tokens) but later on interpreted "ref counting" as "trust lines".
This mistake gave me an idea though, so not all is lost :)
If we were creating `LiquidityPoolEntry` when the trustline is created (not on first deposit), and delete it when then last trustline is deleted, this will address my other comment on use of `LiquidityPoolKey`:
Basically the only place where the full ` LiquidityPoolKey` is needed is with `ChangeTrustOp`, everywhere else can use the "liquidity pool identifier" (derived from the hash of the key).
This will "by design" ensure that any operation that uses pool identifiers will work because all those operations would fail anyways if there was no trustline.
There is therefore no assumption on pool's age.
In operation semantics like deposit, the check `if exists(lp)` gets replaced by `if lp.constantProduct().totalPoolShares != 0` (lp is guaranteed to exist if `tlPool` exists)
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/96c4933f-c947-4ed3-a6ee-b004c35dce2bn%40googlegroups.com.
Use of `LiquidityPoolKey`:
In the future I imagine that we'll have
- Other algo/curve choices
I don't know how many parameters we'll need to identify a pool, but this may get fairly big: there will be the base fee, potentially other things that restrict amounts, time etc.
- More than 1 "constant product" instance (parameters)
It seems to me that we should try to make it more efficient by default.
If we assume that pools tend to be "long lived", we can take advantage of it: if we had an explicit "createpool" (that is idempotent) that creates a pool with the full `LiquidityPoolKey`, we can then use a "liquidity pool identifier" everywhere else something like H(LiquidityPoolKey).
If we do that, we could add right away a fee (expressed in basis points for example) in the `LiquidityPoolKey` (which saves us from having to introduce a new complete type just for that later on) - we would only allow to set it to a hard coded constant in the first version (and open it up to more values later on).
--
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/99b87948-cb52-4728-960f-ba2c664bd08dn%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/10da88c3-9501-4b3c-a305-fd80b5684cc8n%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/d012a47e-5465-4625-ae23-98bf0848a00cn%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CABf4b3y1VXifW6GbJ6ccA69Q5uz9ZyZcVHvKBxQbXoR6Yo-mww%40mail.gmail.com.
Regardless of how we handle liabilities, I don't think restricting supply actually mitigates any issues with Siddharth's proposal 1. You claim, "If we had that, revoking auth on a trustline would just trigger withdrawal from all pools and would not need balance entries (as trustlines would always be able to hold balances)." But this isn't true.- If we don't change how liabilities work, then I can always configure my trust line to have balance + buyingLiabilities = INT64_MAX even if the supply is less than INT64_MAX.- If we do change how liabilities work such that liabilities are limited by the available supply, then if available supply > INT64_MAX/2 it is still possible to have scenarios where balance + buyingLiabilities + amount from pool > INT64_MAX.I don't see an obvious way to circumvent this reality, although I haven't thought too deeply about it. But these scenarios indicate that restricting supply alone is not sufficient to avoid the use of claimable balances.
Another interesting possibility stems from Nicolas' remark, "I think it might be useful to have a flag anyways for assets to disallow trading in pools (or in the DEX) so this would piggyback on that behavior."
Imagine that we added a flag so issuers can disallow trading in pools. For simplicity, this can be an authorization flag on the issuing account (default false) meaning issuers have to explicitly enable trading in liquidity pools. Then the issue with Siddharth's proposal 2 that "right now revoking trust on a trustline is the only thing that people need to do, so in order to stay compliant issuers would have to perform additional logic" is avoided: enabling trading in liquidity pools means that the issuer understands and accepts the new authorization requirements. Adding this flag also has the nice advantage that it increases backwards compatibility for issuers, and allows them to avoid liquidity pools if they have compliance (or other) concerns.
What do you think about this approach?
```
+ Hash liquidityPoolID;
```
Nit: should typedef to avoid using `Hash` (that way we can avoid comments everywhere `SHA256(…)` to remind people of the definition).
```
+union
LiquidityPoolEntry switch (LiquidityPoolType type)
+{
+case LIQUIDITY_POOL_CONSTANT_PRODUCT:
+ struct
+ {
+ Hash liquidityPoolID; // SHA256(LiquidityPoolKey)
```
I think this is fine to include this hash here even though it's not technically required.
Technically speaking I think that looking at the proposal, we should be defining this hash as `LiquidityPoolID` and define the key to be this ID.
Note that you have a bug in the definition of `LedgerKey` (where you really want to use the Hash):
```
case OFFER:
@@ -479,6 +542,9 @@ case CLAIMABLE_BALANCE:
{
ClaimableBalanceID
balanceID;
}
claimableBalance;
+
+case LIQUIDITY_POOL:
+ LiquidityPoolKey
liquidityPool;
};
```
Related: I think that the type `LiquidityPoolKey` that contains all the parameters needed to create a pool should be renamed as `LiquidityPoolParameters` to avoid any possible confusion, and moved to Stellar-Transaction.x (as it's the only place where we should use it).
Alternatively, something maybe a bit cleaner:
Define in `Stellar-ledger-entries.x`:
```
struct LiquidityPoolConstantProductParams {
Asset assetA; // assetA < assetB
Asset assetB;
uint32 fee; // Fee is fee/UINT32_MAX
};
```
And actually use it:
```
union
LiquidityPoolEntry switch (LiquidityPoolType type)
{
case LIQUIDITY_POOL_CONSTANT_PRODUCT:
struct
{
LiquidityPoolID id;
LiquidityPoolConstantProductParams params;
…
```
Then, use it again in `Stellar-transactions.x` to define `LiquidityPoolParameters`:
```
union LiquidityPoolParameters switch (LiquidityPoolType type)
{
case
LIQUIDITY_POOL_CONSTANT_PRODUCT:
LiquidityPoolConstantProductParams constantProduct;
…
```
LiquidityPoolDepositOp
There is mention of `poolEnabled` but it's not defined anywhere.
Related: as `AUTH_LIQUIDITY_POOL_ENABLED_FLAG` is only used in the context of "AUTH REQUIRED" assets (I mentioned that before), I imagine that poolEnabled could be defined as `!(AUTH_REQUIRED_FLAG set) || (AUTH_LIQUIDITY_POOL_ENABLED_FLAG set)` (which also allows to derive its value if the issuer account does not exist and allows those tokens to be in pools).
ChangeTrustOp
We should define a constant (in `Stellar-transactions.x`) instead of using `12884902 ` directly.
It would be clearer to define `LiquidyPoolID` just once instead of repeating how it's computed.
On that topic: we should probably properly domain separate this ID, so adding a new value to `EnvelopeType`.
Nit: rules are the same than for other types (it's worded as if rules were different). For example, can't delete a pool trustline if balance != 0.
SetTrustLineFlagsOp
> SetTrustLineFlagsOp is the only way for an issuer to redeem it's assets from a Liquidity Pool back to the owner, or a claimable balance for the owner to claim.
I can't really parse that sentence and what the intent is. I think it was just trying to explain what happens when auth is revoked.
This operation should probably take a `LiquidityPoolID` as a parameter as this is how pool trustlines are identified after they're created? This avoids having to duplicate parameter checking code that should really only exist in ChangeTrustOp.
Code doesn't deal with native (I think this is a pending issue for the CAP overall that assumes trustlines everywhere).
SetOptions
Missing changes that explain the rules governing changing `AUTH_LIQUIDITY_POOL_ENABLED_FLAG`.
1. I think pool shares should be transferrable.
- It doesn't seem like it would be a significant change to make them transferrable, so I don't really see the point of leaving it for later. Pool shares are incredibly valuable for their utility as a relatively stable collateral asset that still generates yield for the borrower. I know lending isn't big on Stellar right now, but that will hopefully change in the near future, especially with the progress being made on TSS.
4. I didn't see the Authorized_to_Maintain_Liabilities flag mentioned in the CAP. A user who is authorized to maintain liabilities should be allowed to redeem pool shares including the controlled asset but not allowed to deposit the controlled asset into a pool. This avoids the vulnerability where a user who is authorized to maintain liabilities deposits the controlled assets in a pool then sends the pool shares to another party for them to redeem, thereby circumventing the flag (this assumes that pool shares will be transferrable at some point).
7. I'm not convinced that the CAP should avoid interleaved execution.
- Interleaved execution is better for users.
- Regular users now have to decide whether to buy assets using a manageBuyOffer or a pathPayment. This is probably confusing, even though pathPayments will almost always be the better choice.
- Market makers now have to incorporate pathPayments into their strategies as they cannot expect their price floor to purchase assets from AMMs that begin trading below the price floor.
- Interleaved execution is important for maximizing AMM capital efficiency. Preventing AMM's from filling orderbook offers reduces the liquidity they add to the ecosystem. Additionally, based on my back-testing, most AMM yield is driven by the AMM filling a large offer as it handles slippage more efficiently than the orderbook does. While under the proposed system, most large offers may take place through pathPayments, it still concerns me that we're consciously limiting AMM's ability to generate yield and improve liquidity.
- I don't think I see the issue with the exchange trading with liquidity pools. Wouldn't this just result in a more efficient market? If the exchange and the AMM are trading with each other, one of them has an inaccurate price.
- I'm not sure I totally follow the point that interleaved execution should not be allowed due to the Stellar Protocol preventing orders from crossing. The protocol only prevents orders from crossing for a single account, correct? Are you concerned that a user will place an order that crosses the current price of a pool they own shares of? Shouldn't that be a pretty easy error condition to add?
- Given that offers can already be fulfilled by multiple counter offers, would it be possible to handle interleaved execution by
Fair warning, I'm not an expert on Stellar Core Development, so some of my comments may be off-base. Regardless, I hope they help further the discussion.
- Calculating the maxCounterAssetWithdrawn for the AMM given the input amount
- Check if the best orderbook offer will fulfill the trade at a better rate
- Subtract the amount needed to fill the best orderbook offer from the input amount
- If the best orderbook offer does not provide a better rate, fill the input offer with the AMM.
- Repeat until the input offer has been filled as much as is possible given the current market and liquidity pool conditions.
- Apply input offer.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/063eb5a3-cde1-4f27-b1c4-52a56662f944n%40googlegroups.com.
Re: Claimable balances on auth revocation
When authorization is revoked and pool shares are redeemed in the current proposal, claimable balances will be created if the balance cannot be sent back to the owner (this can happen due to a few reasons like hitting limits or deleted trustlines). One question in regards to this mechanism that we are still considering is who puts up the reserves for the claimable balances? In the current proposal, the issuer will have to put up the reserves on auth revocation, but this isn’t ideal as it’s possible for the owner to never redeem the balance.
An idea that Jon had was to increase the number of base reserves for a pool trustline to two. On revocation, the pool shares are redeemed and the trustline is deleted. The owner now has two available base reserves to sponsor the claimable balances. Now that the issuer doesn’t have the responsibility of sponsoring many claimable balances, we can also get rid of the best effort logic where we first try to send the balance back, and then create the claimable balance if that doesn’t work, and instead always create the claimable balance. This will make the effects of auth revocation easier to understand downstream.
There are some minor details to work out (like what happens if the owner is already sponsoring UINT32_MAX entries or if the cost of a base reserve increases), but this approach is better as the owner has an incentive to claim the balance they are sponsoring.
Siddharth# Carry over from my previous review:
## Pool ID definition
There should be a typedef to avoid using `Hash` everywhere a pool ID is expected (that way we can avoid comments everywhere `SHA256(…)` to remind people of the definition and it makes it less ambiguous on where the hash is coming from).
So something like
```
typedef PoolID Hash; // SHA256(LiquidityPoolKey)
```
## Pool key/parameters
I think that the type `LiquidityPoolKey` that contains all the parameters needed to create a pool should be renamed as `LiquidityPoolParameters` to avoid any possible confusion, and moved to Stellar-Transaction.x (as it's the only place where we should use it).
Alternatively, something maybe a bit cleaner:
Define in `Stellar-ledger-entries.x`:
```
struct LiquidityPoolConstantProductParams {
Asset assetA; // assetA < assetB
Asset assetB;
uint32 fee; // Fee is fee/UINT32_MAX
};
```
And actually use it:
```
union
LiquidityPoolEntry switch (LiquidityPoolType type)
{
case LIQUIDITY_POOL_CONSTANT_PRODUCT:
struct
{
LiquidityPoolID id;
LiquidityPoolConstantProductParams params;
…
```
Then, use it again in `Stellar-transactions.x` to define `LiquidityPoolParameters`:
```
union LiquidityPoolParameters switch (LiquidityPoolType type)
{
case
LIQUIDITY_POOL_CONSTANT_PRODUCT:
LiquidityPoolConstantProductParams constantProduct;
…
```
## Use of constants
We should define a constant (in `Stellar-transactions.x`) instead of using `12884902 ` directly.
# new questions
## Fee representation
Fee defined as `uint32 fee; // Fee is fee/UINT32_MAX`
Any reason to define it like this given that fees are defined by humans and that there is already a convention used to express fees?
We could avoid rounding error on the fee by just defining the fee as:
```
int32 feeBasisPoints; // Fee is fee/10000
```
(CAP37 was doing something like that too before switching to dynamic fees)
## LiquidityPoolEntry
`numTrustLines` could benefit from a name that is less ambiguous (we're talking pool shares trust lines here), so maybe rename to `poolSharesTrustLineCount` (that works well with the previous field `
totalPoolShares`).
## ClaimableBalanceID change
There is no need to add
```
union ClaimableBalanceID switch (ClaimableBalanceIDType type)
{
case CLAIMABLE_BALANCE_ID_TYPE_V0:
Hash v0;
+case CLAIMABLE_BALANCE_ID_TYPE_FROM_POOL_REVOKE:
+ Hash fromPoolRevoke;
};
```
This extension point is there in case we want to use something other than a Hash to identify a `ClaimableBalance`.
Hash collision is already taken care of by introducing a new `OperationID`.
## Trustline changes
`numPoolsInvolved` is not a great name (I had to lookup what was the meaning in the CAP). Maybe something more direct like `liquidityPoolUseCount`?
Related, grep for "involved" in the rest of the doc and use something more precise/neutral like "referenced" or "used".
## "Upgrade flags"
`MASK_UPGRADE_FLAGS` is not a great name as it lives in the `LedgerHeader` and does not refer to an upgrade.
As the field is called "flags" the mask and enum should be derived from "Ledger header flags".
So `MASK_LEDGERHEADER_FLAGS` and `LedgerHeaderFlags` for the enum.
Also, consider renaming `DISABLE_LIQUIDITY_POOL_TRADING` to `DISABLE_LIQUIDITY_POOL_TRADING_FLAG` (consistent with how flags are named elsewhere).
## Semantics
### General readability
I think sections here are well detailed - what is missing in each is a "mini intro" of sorts that explains in one sentence what the changes are and (if possible) invariants.
For example, `ChangeTrustOp` should start with "This is the only operation that creates `LiquidityPoolEntry`". Also as other operations need pools to exist, it's probably a good idea to start with this operation.
### `ChangeTrustOp`
Using "trust line" makes the logic a little harder to follow, it would make things simpler to read if they were specified like "pool shares trust line".
The rules applied during creation and deletion of the pool trust line are mixed/interleaved. It would be much better to break things up under different `if` blocks.
### Upgrades
(this section should really be called "ledger header flags"")
Should you also add a flag to stop deposits? If there are rounding bugs in the deposit/withdraw flow, a bad actor could perform a many deposit + withdraw in rapid succession in order to take advantage of the bug (this is true of any AMM implementation).
For the later, this would imply adding a new error code to "deposit" that the operation is not possible somehow.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/206ead11-fa60-45f5-9301-140b0786407fn%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/stellar-dev/CABf4b3wZc6Mdb8fnhDJYRMEh%2BFuzT6mpZtZ3PKzOyZEmk6h1RQ%40mail.gmail.com.
## Pool ID definition
There should be a typedef to avoid using `Hash` everywhere a pool ID is expected (that way we can avoid comments everywhere `SHA256(…)` to remind people of the definition and it makes it less ambiguous on where the hash is coming from).
So something like
```
typedef PoolID Hash; // SHA256(LiquidityPoolKey)
```
## LiquidityPoolEntry
`numTrustLines` could benefit from a name that is less ambiguous (we're talking pool shares trust lines here), so maybe rename to `poolSharesTrustLineCount` (that works well with the previous field `
totalPoolShares`).
## Trustline changes
`numPoolsInvolved` is not a great name (I had to lookup what was the meaning in the CAP). Maybe something more direct like `liquidityPoolUseCount`?
Related, grep for "involved" in the rest of the doc and use something more precise/neutral like "referenced" or "used".
## "Upgrade flags"
`MASK_UPGRADE_FLAGS` is not a great name as it lives in the `LedgerHeader` and does not refer to an upgrade.
As the field is called "flags" the mask and enum should be derived from "Ledger header flags".
So `MASK_LEDGERHEADER_FLAGS` and `LedgerHeaderFlags` for the enum.
Also, consider renaming `DISABLE_LIQUIDITY_POOL_TRADING` to `DISABLE_LIQUIDITY_POOL_TRADING_FLAG` (consistent with how flags are named elsewhere).
### `ChangeTrustOp`
Using "trust line" makes the logic a little harder to follow, it would make things simpler to read if they were specified like "pool shares trust line".
The rules applied during creation and deletion of the pool trust line are mixed/interleaved. It would be much better to break things up under different `if` blocks.
### Upgrades
(this section should really be called "ledger header flags"")
Should you also add a flag to stop deposits? If there are rounding bugs in the deposit/withdraw flow, a bad actor could perform a many deposit + withdraw in rapid succession in order to take advantage of the bug (this is true of any AMM implementation).
For the later, this would imply adding a new error code to "deposit" that the operation is not possible somehow.