Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ProtoRev): Updating Binary Search Range with CL Pools #5930

Merged
merged 18 commits into from
Aug 4, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#5831](https://github.com/osmosis-labs/osmosis/pull/5831) Fix superfluid_delegations query
* [#5835](https://github.com/osmosis-labs/osmosis/pull/5835) Fix println's for "amountZeroInRemainingBigDec before fee" making it into production
* [#5841] (https://github.com/osmosis-labs/osmosis/pull/5841) Fix protorev's out of gas erroring of the user's transcation.
* [#5930] (https://github.com/osmosis-labs/osmosis/pull/5930) Updating Protorev Binary Search Range Logic with CL Pools

### Misc Improvements

Expand Down
8 changes: 7 additions & 1 deletion app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,13 @@ func (appKeepers *AppKeepers) InitNormalKeepers(
protorevKeeper := protorevkeeper.NewKeeper(
appCodec, appKeepers.keys[protorevtypes.StoreKey],
appKeepers.GetSubspace(protorevtypes.ModuleName),
appKeepers.AccountKeeper, appKeepers.BankKeeper, appKeepers.GAMMKeeper, appKeepers.EpochsKeeper, appKeepers.PoolManagerKeeper)
appKeepers.AccountKeeper,
appKeepers.BankKeeper,
appKeepers.GAMMKeeper,
appKeepers.EpochsKeeper,
appKeepers.PoolManagerKeeper,
appKeepers.ConcentratedLiquidityKeeper,
)
appKeepers.ProtoRevKeeper = &protorevKeeper

txFeesKeeper := txfeeskeeper.NewKeeper(
Expand Down
6 changes: 3 additions & 3 deletions x/protorev/keeper/epoch_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ func (s *KeeperTestSuite) TestUpdateHighestLiquidityPools() {
// There are 2 pools with epochTwo and uosmo as denoms,
// One in the GAMM module and one in the Concentrated Liquidity module.
// pool with ID 48 has a liquidity value of 1,000,000
// pool with ID 49 has a liquidity value of 2,000,000
// pool with ID 49 should be returned as the highest liquidity pool
// pool with ID 50 has a liquidity value of 2,000,000
// pool with ID 50 should be returned as the highest liquidity pool
// We provide epochTwo as the input base denom, to test the method chooses the correct pool
// across the GAMM and Concentrated Liquidity modules
name: "Get highest liquidity pools for one GAMM pool and one Concentrated Liquidity pool",
Expand All @@ -162,7 +162,7 @@ func (s *KeeperTestSuite) TestUpdateHighestLiquidityPools() {
},
expectedBaseDenomPools: map[string]map[string]keeper.LiquidityPoolStruct{
"epochTwo": {
"uosmo": {Liquidity: sdk.Int(sdk.NewUintFromString("999999000000000001000000000000000000")), PoolId: 49},
"uosmo": {Liquidity: sdk.Int(sdk.NewUintFromString("999999000000000001000000000000000000")), PoolId: 50},
},
},
},
Expand Down
6 changes: 3 additions & 3 deletions x/protorev/keeper/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@ func (s *KeeperTestSuite) TestSwapping() {
param: param{
expectedTrades: []types.Trade{
{
Pool: 49,
Pool: 50,
TokenIn: "uosmo",
TokenOut: "epochTwo",
},
},
executeSwap: func() {

route := []poolmanagertypes.SwapAmountInRoute{{PoolId: 49, TokenOutDenom: "epochTwo"}}
route := []poolmanagertypes.SwapAmountInRoute{{PoolId: 50, TokenOutDenom: "epochTwo"}}

_, err := s.App.PoolManagerKeeper.RouteExactAmountIn(s.Ctx, s.TestAccs[0], route, sdk.NewCoin("uosmo", sdk.NewInt(10)), sdk.NewInt(1))
s.Require().NoError(err)
Expand Down Expand Up @@ -606,7 +606,7 @@ func (s *KeeperTestSuite) TestStoreJoinExitPoolSwaps() {
{
name: "Non-Gamm Pool, Return Early Do Not Store Any Swaps",
param: param{
poolId: 49,
poolId: 50,
denom: "uosmo",
isJoin: true,
expectedSwap: types.Trade{},
Expand Down
29 changes: 16 additions & 13 deletions x/protorev/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ type (
storeKey storetypes.StoreKey
paramstore paramtypes.Subspace

accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
gammKeeper types.GAMMKeeper
epochKeeper types.EpochKeeper
poolmanagerKeeper types.PoolManagerKeeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
gammKeeper types.GAMMKeeper
epochKeeper types.EpochKeeper
poolmanagerKeeper types.PoolManagerKeeper
concentratedLiquidityKeeper types.ConcentratedLiquidityKeeper
}
)

Expand All @@ -36,21 +37,23 @@ func NewKeeper(
gammKeeper types.GAMMKeeper,
epochKeeper types.EpochKeeper,
poolmanagerKeeper types.PoolManagerKeeper,
concentratedLiquidityKeeper types.ConcentratedLiquidityKeeper,
) Keeper {
// set KeyTable if it has not already been set
if !ps.HasKeyTable() {
ps = ps.WithKeyTable(types.ParamKeyTable())
}

return Keeper{
cdc: cdc,
storeKey: storeKey,
paramstore: ps,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
gammKeeper: gammKeeper,
epochKeeper: epochKeeper,
poolmanagerKeeper: poolmanagerKeeper,
cdc: cdc,
storeKey: storeKey,
paramstore: ps,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
gammKeeper: gammKeeper,
epochKeeper: epochKeeper,
poolmanagerKeeper: poolmanagerKeeper,
concentratedLiquidityKeeper: concentratedLiquidityKeeper,
}
}

Expand Down
34 changes: 32 additions & 2 deletions x/protorev/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,20 +888,50 @@ func (s *KeeperTestSuite) setUpPools() {
},
scalingFactors: []uint64{1, 1},
},
{ // Pool 49 - Used for CL testing
initialLiquidity: sdk.NewCoins(
sdk.NewCoin("uosmo", sdk.NewInt(10_000_000_000_000)),
sdk.NewCoin("epochTwo", sdk.NewInt(8_000_000_000_000)),
),
poolParams: stableswap.PoolParams{
SwapFee: sdk.NewDecWithPrec(0, 2),
ExitFee: sdk.NewDecWithPrec(0, 2),
},
scalingFactors: []uint64{1, 1},
},
}

for _, pool := range s.stableSwapPools {
s.createStableswapPool(pool.initialLiquidity, pool.poolParams, pool.scalingFactors)
}

// Create a concentrated liquidity pool for epoch_hook testing
// Pool 49
// Pool 50
s.PrepareConcentratedPoolWithCoinsAndFullRangePosition("epochTwo", "uosmo")

// Create a cosmwasm pool for testing
// Pool 50
// Pool 51
s.PrepareCosmWasmPool()

// Create a concentrated liquidity pool for range testing
// Pool 52
// Create the CL pool
clPool := s.PrepareCustomConcentratedPool(s.TestAccs[2], "epochTwo", "uosmo", 100, sdk.NewDecWithPrec(2, 3))
fundCoins := sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(1000000000000000000)), sdk.NewCoin("epochTwo", sdk.NewInt(1000000000000000000)))
s.FundAcc(s.TestAccs[2], fundCoins)

// Create 100 ticks in the CL pool, 50 on each side
tokensProvided := sdk.NewCoins(sdk.NewCoin("uosmo", sdk.NewInt(10000000)), sdk.NewCoin("epochTwo", sdk.NewInt(10000000)))
amount0Min := sdk.NewInt(0)
amount1Min := sdk.NewInt(0)
lowerTick := int64(0)
upperTick := int64(100)

for i := int64(0); i < 50; i++ {
s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, clPool.GetId(), s.TestAccs[2], tokensProvided, amount0Min, amount1Min, lowerTick-(100*i), upperTick-(100*i))
s.App.ConcentratedLiquidityKeeper.CreatePosition(s.Ctx, clPool.GetId(), s.TestAccs[2], tokensProvided, amount0Min, amount1Min, lowerTick+(100*i), upperTick+(100*i))
}

// Set all of the pool info into the stores
err := s.App.ProtoRevKeeper.UpdatePools(s.Ctx)
s.Require().NoError(err)
Expand Down
2 changes: 1 addition & 1 deletion x/protorev/keeper/posthandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func (s *KeeperTestSuite) TestAnteHandle() {
params: param{
trades: []types.Trade{
{
Pool: 51,
Pool: 53,
TokenOut: "ibc/0CD3A0285E1341859B5E86B6AB7682F023D03E97607CCC1DC95706411D866DF7",
TokenIn: "uosmo",
},
Expand Down
157 changes: 142 additions & 15 deletions x/protorev/keeper/rebalance.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package keeper

import (
"fmt"

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

poolmanagertypes "github.com/osmosis-labs/osmosis/v17/x/poolmanager/types"
Expand Down Expand Up @@ -30,13 +32,10 @@ func (k Keeper) IterateRoutes(ctx sdk.Context, routes []RouteMetaData, remaining

// If the profit is greater than zero, then we convert the profits to uosmo and compare profits in terms of uosmo
if profit.GT(sdk.ZeroInt()) {
if inputCoin.Denom != types.OsmosisDenomination {
uosmoProfit, err := k.ConvertProfits(ctx, inputCoin, profit)
if err != nil {
k.Logger(ctx).Error("Error converting profits: ", err)
continue
}
profit = uosmoProfit
profit, err := k.ConvertProfits(ctx, inputCoin, profit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may also need to add bounding in ConvertProfits. If a CL pool is the highest liquidity pool then we will swap the entire amount without any tick bounds.

If a CL pool is the highest liquidity than I imagine this won't run into any issues, but maybe good to note in as a comment if we don't want to add the tick checking here that we are not doing so.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding a comment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in saying we went for the former and didn't just add a comment right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait lol adding comment rn, misread the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per convo offline, we decided to not make the check. More likely than not we will not run into the issue of over-stepping MaxTicksCrossed in ConvertProfits. The upper bound check should be sufficient enough to cover most cases.

if err != nil {
k.Logger(ctx).Error("Error converting profits: ", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error won't propagate properly because it accepts kv pairs, let's follow this format:

ctx.Logger().Error("ProtoRevTrade failed with error: " + err.Error())

Realized I missed all places that use the format k.Logger(ctx).Error, I replaced all the ctx.Logger().Error instances, can we add this fix in this PR too for all k.Logger(ctx).Error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotchu fam

continue
}

// Select the optimal route King of the Hill style (route with the highest profit will be executed)
Expand All @@ -53,6 +52,10 @@ func (k Keeper) IterateRoutes(ctx sdk.Context, routes []RouteMetaData, remaining

// ConvertProfits converts the profit denom to uosmo to allow for a fair comparison of profits
func (k Keeper) ConvertProfits(ctx sdk.Context, inputCoin sdk.Coin, profit sdk.Int) (sdk.Int, error) {
if inputCoin.Denom == types.OsmosisDenomination {
return profit, nil
}

// Get highest liquidity pool ID for the input coin and uosmo
conversionPoolID, err := k.GetPoolForDenomPair(ctx, types.OsmosisDenomination, inputCoin.Denom)
if err != nil {
Expand Down Expand Up @@ -132,8 +135,11 @@ func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, rema
return sdk.Coin{}, sdk.ZeroInt(), err
}

// Extend the search range if the max input amount is too small
curLeft, curRight = k.ExtendSearchRangeIfNeeded(ctx, route, inputDenom, curLeft, curRight)
// Update the search range if the max input amount is too small/large
curLeft, curRight, err = k.UpdateSearchRangeIfNeeded(ctx, route, inputDenom, curLeft, curRight)
if err != nil {
return sdk.Coin{}, sdk.ZeroInt(), err
}

// Binary search to find the max profit
for iteration := 0; curLeft.LT(curRight) && iteration < types.MaxIterations; iteration++ {
Expand Down Expand Up @@ -167,30 +173,151 @@ func (k Keeper) FindMaxProfitForRoute(ctx sdk.Context, route RouteMetaData, rema
return tokenIn, profit, nil
}

// UpdateSearchRangeIfNeeded updates the search range for the binary search. First, we check if there are any
// concentrated liquidity pools in the route. If there are, then we may need to reduce the upper bound of the
// binary search since it is gas intensive to move across several ticks. Next, we determine if the current bound
// includes the optimal amount in. If it does not, then we can extend the search range to capture more profits.
func (k Keeper) UpdateSearchRangeIfNeeded(
ctx sdk.Context,
route RouteMetaData,
inputDenom string,
curLeft, curRight sdk.Int,
) (sdk.Int, sdk.Int, error) {
// Retrieve all concentrated liquidity pools in the route
clPools := make([]uint64, 0)
inputMap := make(map[uint64]string)
prevInput := inputDenom

for _, route := range route.Route {
pool, err := k.poolmanagerKeeper.GetPool(ctx, route.PoolId)
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

if pool.GetType() == poolmanagertypes.Concentrated {
clPools = append(clPools, route.PoolId)
inputMap[route.PoolId] = prevInput
}

prevInput = route.TokenOutDenom
}

// If there are concentrated liquidity pools in the route, then we may need to reduce the upper bound of the binary search.
updatedMax, err := k.ReduceSearchRangeIfNeeded(ctx, clPools, inputDenom, inputMap, curLeft, curRight)
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

if updatedMax.LT(curRight) {
return curLeft, updatedMax, nil
}

return k.ExtendSearchRangeIfNeeded(ctx, route, inputDenom, curLeft, curRight, updatedMax)
}

// ReduceSearchRangeIfNeeded returns the max amount in that can be used for the binary search
// respecting the max ticks moved across all concentrated liquidity pools in the route.
func (k Keeper) ReduceSearchRangeIfNeeded(
ctx sdk.Context,
clPools []uint64,
inputDenom string,
inputMap map[uint64]string,
curLeft, curRight sdk.Int,
) (sdk.Int, error) {
// Find the max amount in that can be used for the binary search (in terms of uosmo)
maxInputAmount, stepSize, err := k.MaxInputAmount(ctx)
if err != nil {
return sdk.ZeroInt(), err
}

// Iterate through all CL pools and determine the maximal amount of input that can be used
// respecting the max ticks moved.
for _, poolId := range clPools {
maxInForPool, _, err := k.concentratedLiquidityKeeper.ComputeMaxInAmtGivenMaxTicksCrossed(
ctx,
poolId,
inputMap[poolId],
types.MaxTicksMoved,
)

// In the case where we cannot calculate the max in amount, we short circuit the search
// and return an upper bound of 0. This will cause the binary search to not run.
if err != nil {
return sdk.ZeroInt(), err
}

maxInForPoolInOsmo, err := k.ConvertProfits(ctx, maxInForPool, maxInForPool.Amount)
if err != nil {
return sdk.ZeroInt(), err
}

if maxInForPoolInOsmo.LT(maxInputAmount) {
maxInputAmount = maxInForPoolInOsmo
}
}

return maxInputAmount.Quo(stepSize), nil
}

// Determine if the binary search range needs to be extended
func (k Keeper) ExtendSearchRangeIfNeeded(ctx sdk.Context, route RouteMetaData, inputDenom string, curLeft, curRight sdk.Int) (sdk.Int, sdk.Int) {
func (k Keeper) ExtendSearchRangeIfNeeded(
ctx sdk.Context,
route RouteMetaData,
inputDenom string,
curLeft, curRight, updatedMax sdk.Int,
) (sdk.Int, sdk.Int, error) {
// Get the profit for the maximum amount in
_, maxInProfit, err := k.EstimateMultihopProfit(ctx, inputDenom, curRight.Mul(route.StepSize), route.Route)
if err != nil {
return curLeft, curRight
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

// If the profit for the maximum amount in is still increasing, then we can increase the range of the binary search
if maxInProfit.GTE(sdk.ZeroInt()) {
maxInPlusOne := curRight.Add(sdk.OneInt()).Mul(route.StepSize)
if updatedMax.Mul(route.StepSize).LT(maxInPlusOne) {
return curLeft, curRight, nil
}

// Get the profit for the maximum amount in + 1
_, maxInProfitPlusOne, err := k.EstimateMultihopProfit(ctx, inputDenom, curRight.Add(sdk.OneInt()).Mul(route.StepSize), route.Route)
_, maxInProfitPlusOne, err := k.EstimateMultihopProfit(ctx, inputDenom, maxInPlusOne, route.Route)
if err != nil {
return curLeft, curRight
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

// Change the range of the binary search if the profit is still increasing
if maxInProfitPlusOne.GT(maxInProfit) {
curLeft = curRight
curRight = types.ExtendedMaxInputAmount
curRight = updatedMax
}
}

return curLeft, curRight
return curLeft, curRight, nil
}

// MaxInputAmount returns the max input amount that can be used in any route. We use uosmo as the base
// unit of account for the max input amount.
func (k Keeper) MaxInputAmount(ctx sdk.Context) (sdk.Int, sdk.Int, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re: our in-person sync, if we go the reverse iteration method I don't think we need to choose any base unit of account for this so should be able to remove

baseDenom, err := k.GetAllBaseDenoms(ctx)
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

uosmoStepSize := sdk.ZeroInt()
foundUosmo := false
for _, denom := range baseDenom {
if denom.Denom == types.OsmosisDenomination {
uosmoStepSize = denom.StepSize
foundUosmo = true
break
}
}

if !foundUosmo {
return sdk.ZeroInt(), sdk.ZeroInt(), fmt.Errorf("uosmo not found in base denoms")
}

return types.ExtendedMaxInputAmount.Mul(uosmoStepSize), uosmoStepSize, nil
}

// ExecuteTrade inputs a route, amount in, and rebalances the pool
Expand Down
Loading