poolmanager

package
v0.1.0-merlin.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 9, 2023 License: Apache-2.0 Imports: 12 Imported by: 0

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:

  1. Handle pool creation
    • Assign ids to pools
    • Store the mapping from pool id to one of the swap modules (gamm or concentrated-liquidity)
    • Propagate the execution to the appropriate module depending on the pool type.
    • Note, that pool creation messages are recieved by the pool model's message server. Each module's message server then calls the x/poolmanager keeper method CreatePool.
  2. 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 recieved 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/merlinslair/merlin/blob/f26ceb958adaaf31510e17ed88f5eab47e2bac03/x/poolmanager/types/msg_create_pool.go#L9

For each of balancer, stableswap and concentrated-liquidity pools, we have their own implementation of CreatePoolMsg.

Note the PoolType type. This is an enumeration of all supported pool types. We proto-generate this enumeration:

// proto/merlin/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.

  1. CreatePoolMsg is received by the x/gamm message server.

  2. CreatePool keeper method is called from poolmanager, propagating the appropriate implemenation of the CreatePoolMsg 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) {
    ...
}
  1. The keeper utilizes CreatePoolMsg interface methods to execute the logic specific to each pool type.

  2. 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 neccessary 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 2 swap messages:

  • MsgSwapExactAmountIn
  • MsgSwapExactAmountOut

Their implementation of routing is similar. As a result, we only focus on MsgSwapExactAmountIn.

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,
		swapFee 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 - swapFee) * 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

MsgSwapExactAmountIn

MsgSwapExactAmountOut

MsgSwapExactAmountOut

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. In the moment of the 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 swap fees on each hop would be automatically halved. Example: for converting ATOM -> OSMO -> LUNA using two pools with swap fees 0.3% + 0.2%, instead 0.15% + 0.1% fees will be aplied.

Multi-Hop

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewMsgServerImpl

func NewMsgServerImpl(keeper *Keeper) types.MsgServer

Types

type Keeper

type Keeper struct {
	// contains filtered or unexported fields
}

func NewKeeper

func NewKeeper(storeKey sdk.StoreKey, paramSpace paramtypes.Subspace, gammKeeper types.SwapI, concentratedKeeper types.SwapI, bankKeeper types.BankI, accountKeeper types.AccountI, communityPoolKeeper types.CommunityPoolI) *Keeper

func (Keeper) CreatePool

func (k Keeper) CreatePool(ctx sdk.Context, msg types.CreatePoolMsg) (uint64, error)

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

func (k Keeper) GetNextPoolId(ctx sdk.Context) uint64

GetNextPoolId returns the next pool id.

func (Keeper) GetParams

func (k Keeper) GetParams(ctx sdk.Context) (params types.Params)

GetParams returns the total set of poolmanager parameters.

func (Keeper) GetPoolModule

func (k Keeper) GetPoolModule(ctx sdk.Context, poolId uint64) (types.SwapI, error)

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) 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 (k Keeper) MultihopEstimateInGivenExactAmountOut(
	ctx sdk.Context,
	routes []types.SwapAmountOutRoute,
	tokenOut sdk.Coin,
) (tokenInAmount sdk.Int, err error)

func (Keeper) MultihopEstimateOutGivenExactAmountIn

func (k Keeper) MultihopEstimateOutGivenExactAmountIn(
	ctx sdk.Context,
	routes []types.SwapAmountInRoute,
	tokenIn sdk.Coin,
) (tokenOutAmount sdk.Int, err error)

func (Keeper) RouteExactAmountIn

func (k Keeper) RouteExactAmountIn(
	ctx sdk.Context,
	sender sdk.AccAddress,
	routes []types.SwapAmountInRoute,
	tokenIn sdk.Coin,
	tokenOutMinAmount sdk.Int,
) (tokenOutAmount sdk.Int, err error)

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 (Keeper) RouteExactAmountOut

func (k Keeper) RouteExactAmountOut(ctx sdk.Context,
	sender sdk.AccAddress,
	routes []types.SwapAmountOutRoute,
	tokenInMaxAmount sdk.Int,
	tokenOut sdk.Coin,
) (tokenInAmount sdk.Int, err error)

MultihopSwapExactAmountOut defines the output denom and output amount for the last pool. Calculation starts by providing the tokenOutAmount of the final pool to calculate the required tokenInAmount the calculated tokenInAmount is used as defined tokenOutAmount of the previous pool, calculating in reverse order of the swap Transaction succeeds if the calculated tokenInAmount of the first pool is less than the defined tokenInMaxAmount defined.

func (Keeper) SetNextPoolId

func (k Keeper) SetNextPoolId(ctx sdk.Context, poolId uint64)

SetNextPoolId sets next pool Id.

func (Keeper) SetParams

func (k Keeper) SetParams(ctx sdk.Context, params types.Params)

SetParams sets the total set of poolmanager parameters.

func (*Keeper) SetPoolCreationListeners

func (k *Keeper) SetPoolCreationListeners(listeners types.PoolCreationListeners) *Keeper

SetPoolCreationListeners sets the pool creation listeners.

func (*Keeper) SetPoolIncentivesKeeper

func (k *Keeper) SetPoolIncentivesKeeper(poolIncentivesKeeper types.PoolIncentivesKeeperI)

SetPoolIncentivesKeeper sets pool incentives keeper

func (Keeper) SetPoolRoute

func (k Keeper) SetPoolRoute(ctx sdk.Context, poolId uint64, poolType types.PoolType)

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.

Directories

Path Synopsis
cli
queryproto
Package queryproto is a reverse proxy.
Package queryproto is a reverse proxy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL