Skip to content

Commit

Permalink
fix: improve dust handling in EstimateTradeBasedOnPriceImpact (#6769)
Browse files Browse the repository at this point in the history
* Improve dust handling for balancer pool trade estimation

* Improve dust handling for balancer pool trade estimation

* Update docs

* Add draft CHANGELOG entry

* Update CHANGELOG.md

* Remove use of helper functions

* Place CHANGELOG entry under Unreleased section
  • Loading branch information
migueldingli1997 authored Nov 3, 2023
1 parent 79b0f11 commit 5702bc7
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [#6805](https://github.com/osmosis-labs/osmosis/pull/6805) return bucket index of the current tick from LiquidityPerTickRange query

### Bug Fixes

* [#6769](https://github.com/osmosis-labs/osmosis/pull/6769) fix: improve dust handling in EstimateTradeBasedOnPriceImpact

## v20.0.0

### Features
Expand Down
19 changes: 8 additions & 11 deletions x/poolmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,7 @@ by the routes.

The `EstimateTradeBasedOnPriceImpact` query allows users to estimate a trade for all pool types given the following parameters are provided for this request `EstimateTradeBasedOnPriceImpactRequest`:


- **FromCoin**: (`sdk.Coin`): the total amount of tokens one wants to sell.
- **FromCoin**: (`sdk.Coin`): is the total amount of tokens one wants to sell.
- **ToCoinDenom**: (`string`): is the denom they want to buy with the tokens being sold.
- **PoolId**: (`uint64`): is the identifier of the pool that the trade will happen on.
- **MaxPriceImpact**: (`sdk.Dec`): is the maximum percentage that the user is willing to affect the price of the pool.
Expand All @@ -367,20 +366,19 @@ The following is the process in which the query finds a trade that will stay bel
1. If the `adjustedMaxPriceImpact` was calculated to be `0` or negative it means that the `SpotPrice` is more expensive than the `ExternalPrice` and has already exceeded the possible `MaxPriceImpact`. We return a `sdk.ZeroInt()` input and output for the input and output coins indicating that no trade is viable.
6. Then according to the pool type we attempt to find a viable trade, we must process each pool type differently as they return different results for different scenarios. The sections below explain the different pool types and how they each handle input.


#### Balancer Pool Type Process

The following is the example input/output when executing `CalcOutAmtGivenIn` on balancer pools:

- If the input is greater than the total liquidity of the pool, the output will be the total liquidity of the target token.
- If the input is an amount that is reasonably within the range of liquidity of the pool, the output will be a tolerable slippage amount based on pool data.
- If the input is a small amount for which the pool cannot calculate a viable swap output e.g `1`, the output will be `1`, regardless of slippage.
- If the input is a small amount for which the pool cannot calculate a viable swap output e.g `1`, the output will be a small value which can be either positive (greater or equal to 1) or zero, depending on the pool's weights. In the latter case an `ErrInvalidMathApprox` is returned.

Here is the following process for the `estimateTradeBasedOnPriceImpactBalancerPool` function:
Here is the following process for the `EstimateTradeBasedOnPriceImpactBalancerPool` function:

1. The function initially calculates the output amount (`tokenOut`) using the input amount (`FromCoin`) without including a swap fee using the `CalcOutAmtGivenIn` function.

1. If `tokenOut` is zero, the function returns zero for both the input and output coin, signifying that trading a negligible amount yields no output. It is not likely that this pool type returns a zero but it is still catered for.
1. If `tokenOut` is zero or an `ErrInvalidMathApprox` is returned, the function returns zero for both the input and output coin, signifying that trading a negligible amount yields no output.

2. The function calculates the current trade price (`currTradePrice`) using the initially estimated `tokenOut`. Following that, it calculates the deviation of this price from the spot price (`priceDeviation`).

Expand All @@ -402,7 +400,7 @@ The following is the example input/output when executing `CalcOutAmtGivenIn` on
- If the input is an amount that is reasonably within the range of liquidity of the pool, the output will be a tolerable slippage amount based on pool data.
- If the input is a small amount for which the pool cannot calculate a viable swap output e.g `1`, the function will throw an error.

Here is the following process for the `estimateTradeBasedOnPriceImpactStableSwapPool` function:
Here is the following process for the `EstimateTradeBasedOnPriceImpactStableSwapPool` function:

1. The function begins by attempting to estimate the output amount (`tokenOut`) for a given input amount (`req.FromCoin`). This calculation is done without accounting for the swap fee.

Expand All @@ -424,10 +422,9 @@ Here is the following process for the `estimateTradeBasedOnPriceImpactStableSwap

7. If the new trade amount does not cause an error or panic, and its `priceDeviation` is within limits, the function adjusts the `lowAmount` upwards to continue the search.

8. If the loop completes without finding an acceptable trade amount, the function returns zero coins for both the input and the output.

9. If a viable trade is found, the function performs a final recalculation considering the swap fee and returns the estimated trade.
8. If the loop completes without finding an acceptable trade amount, the function returns zero coins for both the input and the output.

9. If a viable trade is found, the function performs a final recalculation considering the swap fee and returns the estimated trade.

#### Concentrated Liquidity Pool Type Process

Expand All @@ -437,7 +434,7 @@ The following is the example input/output when executing `CalcOutAmtGivenIn` on
- If the input is an amount that is reasonably within the range of liquidity of the pool, the output will be a tolerable slippage amount based on pool data.
- f the input is a small amount for which the pool cannot calculate a viable swap output e.g `1`, the function will return a zero.

Here is the following process for the `estimateTradeBasedOnPriceImpactConcentratedLiquidity` function:
Here is the following process for the `EstimateTradeBasedOnPriceImpactConcentratedLiquidity` function:

1. The function starts by attempting to estimate the output amount (`tokenOut`) for a given input amount (`req.FromCoin`), using the `CalcOutAmtGivenIn` method of the `swapModule`.

Expand Down
27 changes: 20 additions & 7 deletions x/poolmanager/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ package poolmanager
import (
"errors"
"fmt"

"math/big"
"strings"

"github.com/osmosis-labs/osmosis/v20/x/poolmanager/client/queryproto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/osmosis/osmoutils"
gammtypes "github.com/osmosis-labs/osmosis/v20/x/gamm/types"
"github.com/osmosis-labs/osmosis/v20/x/poolmanager/client/queryproto"
"github.com/osmosis-labs/osmosis/v20/x/poolmanager/types"
)

Expand Down Expand Up @@ -741,10 +741,14 @@ func (k Keeper) EstimateTradeBasedOnPriceImpactBalancerPool(
swapModule types.PoolModuleI,
poolI types.PoolI,
) (*queryproto.EstimateTradeBasedOnPriceImpactResponse, error) {
// There isn't a case where the tokenOut could be zero or an error is received but those possibilities are handled
// anyway.
tokenOut, err := swapModule.CalcOutAmtGivenIn(ctx, poolI, req.FromCoin, req.ToCoinDenom, sdk.ZeroDec())
if err != nil {
if errors.Is(err, gammtypes.ErrInvalidMathApprox) {
return &queryproto.EstimateTradeBasedOnPriceImpactResponse{
InputCoin: sdk.NewCoin(req.FromCoin.Denom, sdk.ZeroInt()),
OutputCoin: sdk.NewCoin(req.ToCoinDenom, sdk.ZeroInt()),
}, nil
}
return nil, status.Error(codes.Internal, err.Error())
}
if tokenOut.IsZero() {
Expand All @@ -762,6 +766,12 @@ func (k Keeper) EstimateTradeBasedOnPriceImpactBalancerPool(
ctx, poolI, req.FromCoin, req.ToCoinDenom, poolI.GetSpreadFactor(ctx),
)
if err != nil {
if errors.Is(err, gammtypes.ErrInvalidMathApprox) {
return &queryproto.EstimateTradeBasedOnPriceImpactResponse{
InputCoin: sdk.NewCoin(req.FromCoin.Denom, sdk.ZeroInt()),
OutputCoin: sdk.NewCoin(req.ToCoinDenom, sdk.ZeroInt()),
}, nil
}
return nil, status.Error(codes.Internal, err.Error())
}

Expand Down Expand Up @@ -795,15 +805,18 @@ func (k Keeper) EstimateTradeBasedOnPriceImpactBalancerPool(
midAmount := lowAmount.Add(highAmount).Quo(sdk.NewInt(2))
currFromCoin = sdk.NewCoin(req.FromCoin.Denom, midAmount)

// There isn't a case where the tokenOut could be zero or an error is received but those possibilities are
// handled anyway.
tokenOut, err := swapModule.CalcOutAmtGivenIn(
ctx, poolI, currFromCoin, req.ToCoinDenom, sdk.ZeroDec(),
)
if err != nil {
if errors.Is(err, gammtypes.ErrInvalidMathApprox) {
return &queryproto.EstimateTradeBasedOnPriceImpactResponse{
InputCoin: sdk.NewCoin(req.FromCoin.Denom, sdk.ZeroInt()),
OutputCoin: sdk.NewCoin(req.ToCoinDenom, sdk.ZeroInt()),
}, nil
}
return nil, status.Error(codes.Internal, err.Error())
}

if tokenOut.IsZero() {
return &queryproto.EstimateTradeBasedOnPriceImpactResponse{
InputCoin: sdk.NewCoin(req.FromCoin.Denom, sdk.ZeroInt()),
Expand Down
36 changes: 33 additions & 3 deletions x/poolmanager/router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"reflect"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"

Expand Down Expand Up @@ -1559,9 +1560,10 @@ func (s *KeeperTestSuite) TestEstimateTradeBasedOnPriceImpact() {
maxPriceImpactHalved := sdk.MustNewDecFromStr("0.005") // 0.5%
maxPriceImpactTiny := sdk.MustNewDecFromStr("0.0005") // 0.05%

externalPriceOneBalancer := sdk.MustNewDecFromStr("0.666666667") // Spot Price
externalPriceTwoBalancer := sdk.MustNewDecFromStr("0.622222222") // Cheaper than spot price
externalPriceThreeBalancer := sdk.MustNewDecFromStr("0.663349917") // Transform adjusted max price impact by 50%
externalPriceOneBalancer := sdk.MustNewDecFromStr("0.666666667") // Spot Price
externalPriceOneBalancerInv := math.LegacyOneDec().Quo(externalPriceOneBalancer) // Inverse of externalPriceOneBalancer
externalPriceTwoBalancer := sdk.MustNewDecFromStr("0.622222222") // Cheaper than spot price
externalPriceThreeBalancer := sdk.MustNewDecFromStr("0.663349917") // Transform adjusted max price impact by 50%

externalPriceOneStableSwap := sdk.MustNewDecFromStr("1.00000002") // Spot Price
externalPriceTwoStableSwap := sdk.MustNewDecFromStr("0.98989903") // Cheaper than spot price
Expand Down Expand Up @@ -1624,6 +1626,19 @@ func (s *KeeperTestSuite) TestEstimateTradeBasedOnPriceImpact() {
expectedInputCoin: sdk.NewCoin(assetBaz, sdk.NewInt(39_947)),
expectedOutputCoin: sdk.NewCoin(assetBar, sdk.NewInt(59_327)),
},
"valid balancer pool - estimate trying to trade 1 token": {
preCreatePoolType: types.Balancer,
poolId: poolId,
req: queryproto.EstimateTradeBasedOnPriceImpactRequest{
FromCoin: sdk.NewCoin(assetBar, sdk.NewInt(1)),
ToCoinDenom: assetBaz,
PoolId: poolId,
MaxPriceImpact: maxPriceImpact,
ExternalPrice: externalPriceOneBalancerInv,
},
expectedInputCoin: sdk.NewCoin(assetBar, sdk.NewInt(0)),
expectedOutputCoin: sdk.NewCoin(assetBaz, sdk.NewInt(0)),
},
"valid balancer pool - estimate trying to trade dust": {
preCreatePoolType: types.Balancer,
poolId: poolId,
Expand Down Expand Up @@ -1944,6 +1959,21 @@ func (s *KeeperTestSuite) TestEstimateTradeBasedOnPriceImpact() {
expectedInputCoin: sdk.NewCoin(assetUsdc, sdk.NewInt(0)),
expectedOutputCoin: sdk.NewCoin(assetEth, sdk.NewInt(0)),
},
"valid concentrated pool - estimate trying to trade one unit": {
preCreatePoolType: types.Concentrated,
poolId: poolId,
req: queryproto.EstimateTradeBasedOnPriceImpactRequest{
FromCoin: sdk.NewCoin(assetUsdc, math.OneInt()),
ToCoinDenom: assetEth,
PoolId: poolId,
MaxPriceImpact: maxPriceImpact,
ExternalPrice: externalPriceOneConcentratedInv,
},
setPositionForCLPool: true,
setClTokens: clCoinsLiquid,
expectedInputCoin: sdk.NewCoin(assetUsdc, sdk.NewInt(0)),
expectedOutputCoin: sdk.NewCoin(assetEth, sdk.NewInt(0)),
},
"valid concentrated pool - external price much greater than spot price do not trade": {
preCreatePoolType: types.Concentrated,
poolId: poolId,
Expand Down

0 comments on commit 5702bc7

Please sign in to comment.