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

feat(x/poolmanager): split routes swap message #4886

Merged
merged 26 commits into from
Apr 14, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/ory/dockertest/v3 v3.9.1
github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3
github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230328024000-175ec88e4304
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230405221332-6db5383670e1
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230411200859-ae3065d0ca05
github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304
github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230331072320-5d6f6cfa2627
github.com/pkg/errors v0.9.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -934,8 +934,8 @@ github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3 h1:Ylmch
github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3/go.mod h1:lV6KnqXYD/ayTe7310MHtM3I2q8Z6bBfMAi+bhwPYtI=
github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230328024000-175ec88e4304 h1:iSSlHl+SoewNpP/2N8JaUEHhOQRmJAnS8zaJ11yWslY=
github.com/osmosis-labs/osmosis/osmomath v0.0.3-dev.0.20230328024000-175ec88e4304/go.mod h1:/h3CZIo25kMrM4Ojm7qBgMxKofTVwOycVWSa4rhEsaM=
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230405221332-6db5383670e1 h1:oq28hQA8wHe1uNMHWsoSMJvxMkKQrIeg8PcQD1KkHYg=
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230405221332-6db5383670e1/go.mod h1:zyBrzl2rsZWGbOU+/1hzA+xoQlCshzZuHe/5mzdb/zo=
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230411200859-ae3065d0ca05 h1:fqVGxZPgUWuYWxVcMxHz5vrDV/aoxGJ7Kt0J4Vu/bsY=
github.com/osmosis-labs/osmosis/osmoutils v0.0.0-20230411200859-ae3065d0ca05/go.mod h1:zyBrzl2rsZWGbOU+/1hzA+xoQlCshzZuHe/5mzdb/zo=
github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304 h1:RIrWLzIiZN5Xd2JOfSOtGZaf6V3qEQYg6EaDTAkMnCo=
github.com/osmosis-labs/osmosis/x/epochs v0.0.0-20230328024000-175ec88e4304/go.mod h1:yPWoJTj5RKrXKUChAicp+G/4Ni/uVEpp27mi/FF/L9c=
github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.0-20230331072320-5d6f6cfa2627 h1:A0SwZgp4bmJFbivYJc8mmVhMjrr3EdUZluBYFke11+w=
Expand Down
14 changes: 14 additions & 0 deletions osmoutils/slice_helper.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package osmoutils

import (
"reflect"
"sort"

"golang.org/x/exp/constraints"
Expand Down Expand Up @@ -50,6 +51,19 @@ func ContainsDuplicate[T any](arr []T) bool {
return false
}

// ContainsDuplicateDeepEqual returns true if there are duplicates
// in the slice by performing deep comparison. This is useful
// for comparing matrices or slices of pointers.
// Returns false if there are no deep equal duplicates.
func ContainsDuplicateDeepEqual[T any](multihops []T) bool {
for i := 0; i < len(multihops)-1; i++ {
if reflect.DeepEqual(multihops[i], multihops[i+1]) {
return true
}
}
return false
}

type LessFunc[T any] func(a, b T) bool

// MergeSlices efficiently merges two sorted slices into a single sorted slice.
Expand Down
18 changes: 18 additions & 0 deletions osmoutils/slice_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,21 @@ func TestMergeSlices(t *testing.T) {
})
}
}

func TestContainsDuplicateDeepEqual(t *testing.T) {
tests := []struct {
input []interface{}
want bool
}{
{[]interface{}{[]int{1, 2, 3}, []int{4, 5, 6}}, false},
{[]interface{}{[]int{1, 2, 3}, []int{1, 2, 3}}, true},
{[]interface{}{[]string{"hello", "world"}, []string{"goodbye", "world"}}, false},
{[]interface{}{[]string{"hello", "world"}, []string{"hello", "world"}}, true},
{[]interface{}{[][]int{{1, 2}, {3, 4}}, [][]int{{1, 2}, {3, 4}}}, true},
}

for _, tt := range tests {
got := osmoutils.ContainsDuplicateDeepEqual(tt.input)
require.Equal(t, tt.want, got)
}
}
20 changes: 20 additions & 0 deletions proto/osmosis/poolmanager/v1beta1/swap_route.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,23 @@ message SwapAmountOutRoute {
string token_in_denom = 2
[ (gogoproto.moretags) = "yaml:\"token_in_denom\"" ];
}

message SwapAmountInSplitRoute {
repeated SwapAmountInRoute pools = 1
[ (gogoproto.moretags) = "yaml:\"pools\"", (gogoproto.nullable) = false ];
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
string token_in_amount = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_in_amount\"",
(gogoproto.nullable) = false
];
}

message SwapAmountOutSplitRoute {
repeated SwapAmountOutRoute pools = 1
[ (gogoproto.moretags) = "yaml:\"pools\"", (gogoproto.nullable) = false ];
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
string token_out_amount = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_out_amount\"",
(gogoproto.nullable) = false
];
}
42 changes: 42 additions & 0 deletions proto/osmosis/poolmanager/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ message MsgSwapExactAmountInResponse {
];
}

// ===================== MsgSplitRouteSwapExactAmountIn
message MsgSplitRouteSwapExactAmountIn {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
repeated SwapAmountInSplitRoute routes = 2 [ (gogoproto.nullable) = false ];
string token_in_denom = 3
[ (gogoproto.moretags) = "yaml:\"token_in_denom\"" ];
string token_out_min_amount = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_out_min_amount\"",
(gogoproto.nullable) = false
];
}

message MsgSplitRouteSwapExactAmountInResponse {
string token_out_amount = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_out_amount\"",
(gogoproto.nullable) = false
];
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
}

// ===================== MsgSwapExactAmountOut
message MsgSwapExactAmountOut {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
Expand All @@ -59,3 +80,24 @@ message MsgSwapExactAmountOutResponse {
(gogoproto.nullable) = false
];
}

// ===================== MsgSplitRouteSwapExactAmountOut
message MsgSplitRouteSwapExactAmountOut {
string sender = 1 [ (gogoproto.moretags) = "yaml:\"sender\"" ];
repeated SwapAmountOutSplitRoute routes = 2 [ (gogoproto.nullable) = false ];
string token_out_denom = 3
[ (gogoproto.moretags) = "yaml:\"token_out_denom\"" ];
string token_in_max_amount = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_in_max_amount\"",
(gogoproto.nullable) = false
];
}

message MsgSplitRouteSwapExactAmountOutResponse {
string token_in_amount = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"token_in_amount\"",
(gogoproto.nullable) = false
];
}
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions x/poolmanager/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
"github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
)

var (
IntMaxValue = intMaxValue
)

func (k Keeper) GetNextPoolIdAndIncrement(ctx sdk.Context) uint64 {
return k.getNextPoolIdAndIncrement(ctx)
}
Expand Down
123 changes: 123 additions & 0 deletions x/poolmanager/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package poolmanager
import (
"errors"
"fmt"
"math/big"

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

Expand All @@ -11,6 +12,11 @@ import (
"github.com/osmosis-labs/osmosis/v15/x/poolmanager/types"
)

var (
// 1 << 256 - 1 where 256 is the max bit length defined for sdk.Int
intMaxValue = sdk.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)))
)

// RouteExactAmountIn defines the input denom and input amount for the first pool,
// the output of the first pool is chained as the input for the next routed pool
// transaction succeeds when final amount out is greater than tokenOutMinAmount defined.
Expand Down Expand Up @@ -93,6 +99,64 @@ func (k Keeper) RouteExactAmountIn(
return tokenOutAmount, nil
}

// SplitRouteExactAmountIn routes the swap across multiple multihop paths
// to get the desired token out. This is useful for achieving the most optimal execution. However, note that the responsibility
// of determining the optimal split is left to the client. This method simply routes the swap across the given routes.
//
// It performs the price impact protection check on the combination of tokens out from all multihop paths. The given tokenOutMinAmount
// is used for comparison.
//
// Returns error if:
// - routes are empty
// - routes contain duplicate multihop paths
// - last token out denom is not the same for all multihop paths in route
// - one of the multihop swaps fails for internal reasons
// - final token out computed is not positive
// - final token out computed is smaller than tokenOutMinAmount
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
func (k Keeper) SplitRouteExactAmountIn(
ctx sdk.Context,
sender sdk.AccAddress,
routes []types.SwapAmountInSplitRoute,
tokenInDenom string,
tokenOutMinAmount sdk.Int,
) (sdk.Int, error) {
if err := types.ValidateSwapAmountInSplitRoute(routes); err != nil {
return sdk.Int{}, err
}

var (
// We start the multihop min amount as zero because we want
// to perform a price impact protection check on the combination of tokens out
// from all multihop paths.
multihopStartTokenOutMinAmount = sdk.ZeroInt()
totalOutAmount = sdk.ZeroInt()
)

for _, multihopRoute := range routes {
tokenOutAmount, err := k.RouteExactAmountIn(
ctx,
sender,
types.SwapAmountInRoutes(multihopRoute.Pools),
sdk.NewCoin(tokenInDenom, multihopRoute.TokenInAmount),
multihopStartTokenOutMinAmount)
if err != nil {
return sdk.Int{}, err
}

totalOutAmount = totalOutAmount.Add(tokenOutAmount)
}

if !totalOutAmount.IsPositive() {
return sdk.Int{}, types.FinalAmountIsNotPositiveError{IsAmountOut: true, Amount: totalOutAmount}
}

if totalOutAmount.LT(tokenOutMinAmount) {
return sdk.Int{}, types.PriceImpactProtectionExactInError{Actual: totalOutAmount, MinAmount: tokenOutMinAmount}
}

return totalOutAmount, nil
}

// SwapExactAmountIn is an API for swapping an exact amount of tokens
// as input to a pool to get a minimum amount of the desired token out.
// The method succeeds when tokenOutAmount is greater than tokenOutMinAmount defined.
Expand Down Expand Up @@ -305,6 +369,65 @@ func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
return tokenInAmount, nil
}

// SplitRouteExactAmountOut routes the swap across multiple multihop paths
// to get the desired token in. This is useful for achieving the most optimal execution. However, note that the responsibility
// of determining the optimal split is left to the client. This method simply routes the swap across the given routes.
//
// It performs the price impact protection check on the combination of tokens in from all multihop paths. The given tokenInMaxAmount
// is used for comparison.
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
//
// Returns error if:
// - routes are empty
// - routes contain duplicate multihop paths
// - last token out denom is not the same for all multihop paths in route
// - one of the multihop swaps fails for internal reasons
// - final token out computed is not positive
// - final token out computed is smaller than tokenInMaxAmount
func (k Keeper) SplitRouteExactAmountOut(
ctx sdk.Context,
sender sdk.AccAddress,
routes []types.SwapAmountOutSplitRoute,
tokenOutDenom string,
tokenInMaxAmount sdk.Int,
) (sdk.Int, error) {
if err := types.ValidateSwapAmountOutSplitRoute(routes); err != nil {
return sdk.Int{}, err
}

var (
// We start the multihop min amount as int max value
// that is defined as one under the max bit length of sdk.Int
// which is 256. This is to ensure that we utilize price impact protection
// on the total of in amount from all multihop paths.
multihopStartTokenInMaxAmount = intMaxValue
totalInAmount = sdk.ZeroInt()
)

for _, multihopRoute := range routes {
tokenOutAmount, err := k.RouteExactAmountOut(
ctx,
sender,
types.SwapAmountOutRoutes(multihopRoute.Pools),
multihopStartTokenInMaxAmount,
sdk.NewCoin(tokenOutDenom, multihopRoute.TokenOutAmount))
if err != nil {
return sdk.Int{}, err
}

totalInAmount = totalInAmount.Add(tokenOutAmount)
}

if !totalInAmount.IsPositive() {
return sdk.Int{}, types.FinalAmountIsNotPositiveError{IsAmountOut: false, Amount: totalInAmount}
}

if totalInAmount.GT(tokenInMaxAmount) {
return sdk.Int{}, types.PriceImpactProtectionExactOutError{Actual: totalInAmount, MaxAmount: tokenInMaxAmount}
}

return totalInAmount, nil
}

func (k Keeper) RouteGetPoolDenoms(
ctx sdk.Context,
poolId uint64,
Expand Down
Loading