README ¶
Pool Manager Module
The poolmanager module exists as a swap entrypoint for any pool model that exists on the chain. The poolmanager module is responsible for routing swaps across various pools. It also performs pool-id management for any on-chain pool.
The user-stories for this module follow:
As a user, I would like to have a unified entrypoint for my swaps regardless of the underlying pool implementation so that I don't need to reason about API complexity
As a user, I would like the pool management to be unified so that I don't have to reason about additional complexity stemming from divergent pool sources.
We have multiple pool-storage modules. Namely, x/gamm
and x/concentrated-liquidity
.
To avoid fragmenting swap and pool creation entrypoints and duplicating their boilerplate logic,
we define a poolmanager
module. Its purpose is twofold:
- Handle pool creation
- Assign ids to pools
- Store the mapping from pool id to one of the swap modules (
gamm
orconcentrated-liquidity
) - Propagate the execution to the appropriate module depending on the pool type.
- Note, that pool creation messages are received by the pool model's message server.
Each module's message server then calls the
x/poolmanager
keeper methodCreatePool
.
- Handle swaps
- Cover & share multihop logic
- Propagate intra-pool swaps to the appropriate module depending on the pool type.
- Contrary to pool creation, swap messages are received by the
x/poolmanager
message server.
Let's consider pool creation and swaps separately and in more detail.
Pool Creation & Id Management
To make sure that the pool ids are unique across the two modules, we unify pool id management
in the poolmanager
.
When a call to CreatePool
keeper method is received, we get the next pool id from the module
storage, assign it to the new pool, and propagate the execution to either gamm
or concentrated-liquidity
modules.
Note that we define a CreatePoolMsg
interface:
https://github.com/osmosis-labs/osmosis/blob/f26ceb958adaaf31510e17ed88f5eab47e2bac03/x/poolmanager/types/msg_create_pool.go#L9
Each balancer
, stableswap
and concentrated-liquidity
pool has its own implementation of CreatePoolMsg
.
Note the PoolType
type. This is an enumeration of all supported pool types.
We proto-generate this enumeration:
// proto/osmosis/poolmanager/v1beta1/module_route.proto
// generates to x/poolmanager/types/module_route.pb.go
// PoolType is an enumeration of all supported pool types.
enum PoolType {
option (gogoproto.goproto_enum_prefix) = false;
// Balancer is the standard xy=k curve. Its pool model is defined in x/gamm.
Balancer = 0;
// Stableswap is the Solidly cfmm stable swap curve. Its pool model is defined
// in x/gamm.
StableSwap = 1;
// Concentrated is the pool model specific to concentrated liquidity. It is
// defined in x/concentrated-liquidity.
Concentrated = 2;
}
Let's begin by considering the execution flow of the pool creation message.
Assume balancer
pool is being created.
-
CreatePoolMsg
is received by thex/gamm
message server. -
CreatePool
keeper method is called frompoolmanager
, propagating the appropriate implementation of theCreatePoolMsg
interface.
// x/poolmanager/creator.go CreatePool(...)
// CreatePool attempts to create a pool returning the newly created pool ID or
// an error upon failure. The pool creation fee is used to fund the community
// pool. It will create a dedicated module account for the pool and sends the
// initial liquidity to the created module account.
//
// After the initial liquidity is sent to the pool's account, this function calls an
// InitializePool function from the source module. That module is responsible for:
// - saving the pool into its own state
// - Minting LP shares to pool creator
// - Setting metadata for the shares
func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, error) {
...
}
-
The keeper utilizes
CreatePoolMsg
interface methods to execute the logic specific to each pool type. -
Lastly,
poolmanager.CreatePool
routes the execution to the appropriate module.
The propagation to the desired module is ensured by the routing table stored in memory in the poolmanager
keeper.
// x/poolmanager/keeper.go NewKeeper(...)
func NewKeeper(...) *Keeper {
...
routes := map[types.PoolType]types.SwapI{
types.Balancer: gammKeeper,
types.Stableswap: gammKeeper,
types.Concentrated: concentratedKeeper,
}
return &Keeper{..., routes: routes}
}
MsgCreatePool
interface defines the following method: GetPoolType() PoolType
As a result, poolmanagerkeeper.CreatePool
can route the execution to the appropriate module in
the following way:
// x/poolmanager/creator.go CreatePool(...)
swapModule := k.routes[msg.GetPoolType()]
if err := swapModule.InitializePool(ctx, pool, sender); err != nil {
return 0, err
}
Where swapModule is either gamm
or concentrated-liquidity
keeper.
Both of these modules implement the SwapI
interface:
// x/poolmanager/types/routes.go SwapI interface
type SwapI interface {
...
InitializePool(ctx sdk.Context, pool gammtypes.PoolI, creatorAddress sdk.AccAddress) error
}
As a result, the poolmanager
module propagates core execution to the appropriate swap module.
Lastly, the poolmanager
keeper stores a mapping from the pool id to the pool type.
This mapping is going to be necessary for knowing where to route the swap messages.
To achieve this, we create the following store index:
// x/poolmanager/types/keys.go
var (
...
SwapModuleRouterPrefix = []byte{0x02}
)
// N.B.: we proto-generate this struct. However, the proto
// definition is omitted for brevity.
type ModuleRoute struct {
PoolType PoolType
}
// FormatModuleRouteKey serializes pool id with appropriate prefix into bytes.
func FormatModuleRouteKey(poolId uint64) []byte {
return []byte(fmt.Sprintf("%s%d", SwapModuleRouterPrefix, poolId))
}
// ParseModuleRouteFromBz parses the raw bytes into ModuleRoute.
// Returns error if fails to parse or if the bytes are empty.
func ParseModuleRouteFromBz(bz []byte) (ModuleRoute, error) {
// parsing logic
}
Swaps
There are 4 swap messages:
MsgSwapExactAmountIn
MsgSwapExactAmountOut
MsgSplitRouteSwapExactAmountIn
MsgSplitRouteSwapExactAmountOut
Between, MsgSwapExactAmountIn
and MsgSwapExactAmountOut
, the implementation of routing is similar. We only focus on MsgSwapExactAmountIn
below.
MsgSplitRouteSwapExactAmountIn
and MsgSplitRouteSwapExactAmountOut
support split routes where for each split route they call the respective
MsgSwapExactAmountIn
or MsgSwapExactAmountOut
message. When using the split routes, the slippage protection is disabled on the per-route basis.
For swap exact amount in, we provide zero for the min amount out. For swap exact amount out, we provide the max amount in which is 1 << 256 - 1.
Read more about route splitting in the "Route Splitting" section.
Once the message is received, it calls RouteExactAmountIn
// x/poolmanager/router.go RouteExactAmountIn(...)
// 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.
func (k Keeper) RouteExactAmountIn(
ctx sdk.Context,
sender sdk.AccAddress,
routes []types.SwapAmountInRoute,
tokenIn sdk.Coin,
tokenOutMinAmount sdk.Int) (tokenOutAmount sdk.Int, err error) {
}
Essentially, the method iterates over the routes and calls a SwapExactAmountIn
method
for each, subsequently updating the inter-pool swap state.
The routing works by querying the index SwapModuleRouterPrefix
,
searching up the poolmanagerkeeper.router
mapping, and calling
SwapExactAmountIn
method of the appropriate module.
// x/poolmanager/router.go RouteExactAmountIn(...)
moduleRouteBytes := osmoutils.MustGet(poolmanagertypes.FormatModuleRouteIndex(poolId))
moduleRoute, _ := poolmanagertypes.ModuleRouteFromBytes(moduleRouteBytes)
swapModule := k.routes[moduleRoute.PoolType]
_ := swapModule.SwapExactAmountIn(...)
- note that error checks and other details are omitted for brevity.
Similar to pool creation logic, we are able to call SwapExactAmountIn
on any of the swap
modules by implementing the SwapI
interface:
// x/poolmanager/types/routes.go SwapI interface
type SwapI interface {
...
SwapExactAmountIn(
ctx sdk.Context,
sender sdk.AccAddress,
poolId gammtypes.PoolI,
tokenIn sdk.Coin,
tokenOutDenom string,
tokenOutMinAmount sdk.Int,
spreadFactor sdk.Dec,
) (sdk.Int, error)
}
During the process of swapping a specific asset, the token the user is
putting into the pool is denoted as tokenIn
, while the token that
would be returned to the user, the asset that is being swapped for,
after the swap is denoted as tokenOut
throughout the module.
For example, in the context of balancer pools, given a tokenIn
, the
following calculations are done to calculate how many tokens are to be
swapped into and removed from the pool:
tokenBalanceOut * [1 - { tokenBalanceIn / (tokenBalanceIn + (1 - spreadFactor) * tokenAmountIn)} ^ (tokenWeightIn / tokenWeightOut)]
The calculation is also able to be reversed, the case where user
provides tokenOut
. The calculation for the amount of tokens that the
user should be putting in is done through the following formula:
tokenBalanceIn * [{tokenBalanceOut / (tokenBalanceOut - tokenAmountOut)} ^ (tokenWeightOut / tokenWeightIn) -1] / tokenAmountIn
Existing Swap types:
- SwapExactAmountIn
- SwapExactAmountOut
Messages
MsgSwapExactAmountIn
MsgSwapExactAmountOut
MsgSplitRouteSwapExactAmountIn
MsgSplitRouteSwapExactAmountIn
MsgSplitRouteSwapExactAmountOut
MsgSplitRouteSwapExactAmountOut
Multi-Hop
All tokens are swapped using a multi-hop mechanism. That is, all swaps are routed via the most cost-efficient way, swapping in and out from multiple pools in the process. The most cost-efficient route is determined offline and the list of the pools is provided externally, by user, during the broadcasting of the swapping transaction. At the moment of execution, the provided route may not be the most cost-efficient one anymore.
When a trade consists of just two OSMO-included routes during a single transaction,
the spread factors on each hop would be automatically halved.
Example: for converting ATOM -> OSMO -> LUNA
using two pools with spread factors 0.3% + 0.2%
,
instead 0.15% + 0.1%
spread factors will be applied.
Route Splitting
Each route can be thought of as a separate multi-hop swap.
Splitting swaps across multiple pools for the same token pair can be beneficial for several reasons, primarily relating to reduced slippage, price impact, and potentially lower spreads.
Here's a detailed explanation of these advantages:
-
Reduced slippage: When a large trade is executed in a single pool, it can be significantly affected if someone else executes a large swap against that pool.
-
Lower price impact: When executing a large trade in a single pool, the price impact can be substantial, leading to a less favorable exchange rate for the trader. By splitting the swap across multiple pools, the price impact in each pool is minimized, resulting in a better overall exchange rate.
-
Improved liquidity utilization: Different pools may have varying levels of liquidity, spreads, and price curves. By splitting swaps across multiple pools, the router can utilize liquidity from various sources, allowing for more efficient execution of trades. This is particularly useful when the liquidity in a single pool is not sufficient to handle a large trade or when the price curve of one pool becomes less favorable as the trade size increases.
-
Potentially lower spreads: In some cases, splitting swaps across multiple pools may result in lower overall spreads. This can happen when different pools have different spread structures, or when the total spread paid across multiple pools is lower than the spread for executing the entire trade in a single pool with higher slippage.
Note, that the actual split happens off-chain. The router is only responsible for executing the swaps in the order and quantities of token in provided by the routes.
Documentation ¶
Index ¶
- func NewMsgServerImpl(keeper *Keeper) types.MsgServer
- type Keeper
- func (k Keeper) AllPools(ctx sdk.Context) ([]types.PoolI, error)
- func (k Keeper) CreateConcentratedPoolAsPoolManager(ctx sdk.Context, msg types.CreatePoolMsg) (types.PoolI, error)
- func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, error)
- func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState
- func (k Keeper) GetNextPoolId(ctx sdk.Context) uint64
- func (k Keeper) GetParams(ctx sdk.Context) (params types.Params)
- func (k Keeper) GetPool(ctx sdk.Context, poolId uint64) (types.PoolI, error)
- func (k Keeper) GetPoolModule(ctx sdk.Context, poolId uint64) (types.PoolModuleI, error)
- func (k Keeper) GetTotalPoolLiquidity(ctx sdk.Context, poolId uint64) (sdk.Coins, error)
- func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState)
- func (k Keeper) MultihopEstimateInGivenExactAmountOut(ctx sdk.Context, route []types.SwapAmountOutRoute, tokenOut sdk.Coin) (tokenInAmount sdk.Int, err error)
- func (k Keeper) MultihopEstimateOutGivenExactAmountIn(ctx sdk.Context, route []types.SwapAmountInRoute, tokenIn sdk.Coin) (tokenOutAmount sdk.Int, err error)
- func (k Keeper) RouteCalculateSpotPrice(ctx sdk.Context, poolId uint64, quoteAssetDenom string, baseAssetDenom string) (price sdk.Dec, err error)
- func (k Keeper) RouteExactAmountIn(ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountInRoute, ...) (tokenOutAmount sdk.Int, err error)
- func (k Keeper) RouteExactAmountOut(ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountOutRoute, ...) (tokenInAmount sdk.Int, err error)
- func (k Keeper) RouteGetPoolDenoms(ctx sdk.Context, poolId uint64) (denoms []string, err error)
- func (k Keeper) SetNextPoolId(ctx sdk.Context, poolId uint64)
- func (k Keeper) SetParams(ctx sdk.Context, params types.Params)
- func (k *Keeper) SetPoolIncentivesKeeper(poolIncentivesKeeper types.PoolIncentivesKeeperI)
- func (k Keeper) SetPoolRoute(ctx sdk.Context, poolId uint64, poolType types.PoolType)
- func (k Keeper) SplitRouteExactAmountIn(ctx sdk.Context, sender sdk.AccAddress, routes []types.SwapAmountInSplitRoute, ...) (sdk.Int, error)
- func (k Keeper) SplitRouteExactAmountOut(ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountOutSplitRoute, ...) (sdk.Int, error)
- func (k Keeper) SwapExactAmountIn(ctx sdk.Context, sender sdk.AccAddress, poolId uint64, tokenIn sdk.Coin, ...) (tokenOutAmount sdk.Int, err error)
- func (k Keeper) TotalLiquidity(ctx sdk.Context) (sdk.Coins, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewMsgServerImpl ¶
Types ¶
type Keeper ¶
type Keeper struct {
// contains filtered or unexported fields
}
func NewKeeper ¶
func NewKeeper(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, gammKeeper types.PoolModuleI, concentratedKeeper types.PoolModuleI, cosmwasmpoolKeeper types.PoolModuleI, bankKeeper types.BankI, accountKeeper types.AccountI, communityPoolKeeper types.CommunityPoolI) *Keeper
func (Keeper) AllPools ¶
AllPools returns all pools sorted by their ids from every pool module registered in the pool manager keeper.
func (Keeper) CreateConcentratedPoolAsPoolManager ¶
func (k Keeper) CreateConcentratedPoolAsPoolManager(ctx sdk.Context, msg types.CreatePoolMsg) (types.PoolI, error)
CreateConcentratedPoolAsPoolManager creates a concentrated liquidity pool from given message without sending any initial liquidity to the pool and paying a creation fee. This is meant to be used for creating the pools internally (such as in the upgrade handler). The creator of the pool must be the poolmanager module account. Returns error if not. Otherwise, functions the same as the regular createPoolZeroLiquidityNoCreationFee.
func (Keeper) CreatePool ¶
CreatePool attempts to create a pool returning the newly created pool ID or an error upon failure. The pool creation fee is used to fund the community pool. It will create a dedicated module account for the pool and sends the initial liquidity to the created module account.
After the initial liquidity is sent to the pool's account, this function calls an InitializePool function from the source module. That module is responsible for: - saving the pool into its own state - Minting LP shares to pool creator - Setting metadata for the shares
func (Keeper) ExportGenesis ¶
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState
ExportGenesis returns the poolmanager module's exported genesis.
func (Keeper) GetNextPoolId ¶
GetNextPoolId returns the next pool id.
func (Keeper) GetPoolModule ¶
GetPoolModule returns the swap module for the given pool ID. Returns error if: - any database error occurs. - fails to find a pool with the given id. - the swap module of the type corresponding to the pool id is not registered in poolmanager's keeper constructor. TODO: unexport after concentrated-liqudity upgrade. Currently, it is exported for the upgrade handler logic and tests.
func (Keeper) GetTotalPoolLiquidity ¶
GetTotalPoolLiquidity gets the total liquidity for a given poolId.
func (Keeper) InitGenesis ¶
func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState)
InitGenesis initializes the poolmanager module's state from a provided genesis state.
func (Keeper) MultihopEstimateInGivenExactAmountOut ¶
func (Keeper) MultihopEstimateOutGivenExactAmountIn ¶
func (Keeper) RouteCalculateSpotPrice ¶
func (Keeper) RouteExactAmountIn ¶
func (k Keeper) RouteExactAmountIn( ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountInRoute, tokenIn sdk.Coin, tokenOutMinAmount sdk.Int, ) (tokenOutAmount sdk.Int, err error)
RouteExactAmountIn processes a swap along the given route using the swap function corresponding to poolID's pool type. It takes in the input denom and amount for the initial swap against the first pool and chains the output as the input for the next routed pool until the last pool is reached. Transaction succeeds if final amount out is greater than tokenOutMinAmount defined and no errors are encountered along the way.
func (Keeper) RouteExactAmountOut ¶
func (k Keeper) RouteExactAmountOut(ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountOutRoute, tokenInMaxAmount sdk.Int, tokenOut sdk.Coin, ) (tokenInAmount sdk.Int, err error)
RouteExactAmountOut processes a swap along the given route using the swap function corresponding to poolID's pool type. This function is responsible for computing the optimal output amount for a given input amount when swapping tokens, taking into account the current price of the tokens in the pool and any slippage. Transaction succeeds if the calculated tokenInAmount of the first pool is less than the defined tokenInMaxAmount defined.
func (Keeper) RouteGetPoolDenoms ¶
func (Keeper) SetNextPoolId ¶
SetNextPoolId sets next pool Id.
func (*Keeper) SetPoolIncentivesKeeper ¶
func (k *Keeper) SetPoolIncentivesKeeper(poolIncentivesKeeper types.PoolIncentivesKeeperI)
SetPoolIncentivesKeeper sets pool incentives keeper
func (Keeper) SetPoolRoute ¶
func (Keeper) SplitRouteExactAmountIn ¶
func (k Keeper) SplitRouteExactAmountIn( ctx sdk.Context, sender sdk.AccAddress, routes []types.SwapAmountInSplitRoute, tokenInDenom string, tokenOutMinAmount sdk.Int, ) (sdk.Int, error)
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 route the swap across the given route. The route must end with the same token out and begin with the same token in.
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:
- route are empty
- route contain duplicate multihop paths
- last token out denom is not the same for all multihop paths in routeStep
- one of the multihop swaps fails for internal reasons
- final token out computed is not positive
- final token out computed is smaller than tokenOutMinAmount
func (Keeper) SplitRouteExactAmountOut ¶
func (k Keeper) SplitRouteExactAmountOut( ctx sdk.Context, sender sdk.AccAddress, route []types.SwapAmountOutSplitRoute, tokenOutDenom string, tokenInMaxAmount sdk.Int, ) (sdk.Int, error)
SplitRouteExactAmountOut route 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 route the swap across the given route. The route must end with the same token out and begin with the same token in.
It performs the price impact protection check on the combination of tokens in from all multihop paths. The given tokenInMaxAmount is used for comparison.
Returns error if:
- route are empty
- route contain duplicate multihop paths
- last token out denom is not the same for all multihop paths in routeStep
- 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 (Keeper) SwapExactAmountIn ¶
func (k Keeper) SwapExactAmountIn( ctx sdk.Context, sender sdk.AccAddress, poolId uint64, tokenIn sdk.Coin, tokenOutDenom string, tokenOutMinAmount sdk.Int, ) (tokenOutAmount sdk.Int, err error)
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. Errors otherwise. Also, errors if the pool id is invalid, if tokens do not belong to the pool with given id or if sender does not have the swapped-in tokenIn.