Skip to content

Commit

Permalink
[feat: CL hooks]: Add CL hooks into core CL logic and test hook-speci…
Browse files Browse the repository at this point in the history
…fic behavior (#6859)

* implement hook messages and calls

* add comments and clean up helpers

* osmoutils go mod

* alpo/cl-hooks-wiring

* changelog

* clean up tests

* tighten assertions

* go mod osmoutils
  • Loading branch information
AlpinYukseloglu authored Nov 22, 2023
1 parent d5a8a2b commit 059c5db
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#6758](https://github.com/osmosis-labs/osmosis/pull/6758) Add codec for MsgUndelegateFromRebalancedValidatorSet
* [#6836](https://github.com/osmosis-labs/osmosis/pull/6836) Add DenomsMetadata to stargate whitelist and fixs the DenomMetadata response type
* [#6814](https://github.com/osmosis-labs/osmosis/pull/6814) Add EstimateTradeBasedOnPriceImpact to stargate whitelist
* [#6859](https://github.com/osmosis-labs/osmosis/pull/6859) Add hooks to core CL operations (position creation/withdrawal and swaps)

### Misc Improvements

Expand Down
38 changes: 36 additions & 2 deletions x/concentrated-liquidity/lp.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type CreatePositionData struct {
// - the liquidity delta is zero
// - the amount0 or amount1 returned from the position update is less than the given minimums
// - the pool or user does not have enough tokens to satisfy the requested amount
//
// BeforeCreatePosition hook is triggered after validation logic but before any state changes are made.
// AfterCreatePosition hook is triggered after state changes are complete if no errors have occurred.
func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min, amount1Min osmomath.Int, lowerTick, upperTick int64) (CreatePositionData, error) {
// Use the current blockTime as the position's join time.
joinTime := ctx.BlockTime()
Expand Down Expand Up @@ -89,15 +92,22 @@ func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr
return CreatePositionData{}, err
}

positionId := k.getNextPositionIdAndIncrement(ctx)

// If this is the first position created in this pool, ensure that the position includes both asset0 and asset1
// in order to assign an initial spot price.
hasPositions, err := k.HasAnyPositionForPool(ctx, poolId)
if err != nil {
return CreatePositionData{}, err
}

// Trigger before hook for CreatePosition prior to mutating state.
// If no contract is set, this will be a no-op.
err = k.BeforeCreatePosition(ctx, poolId, owner, tokensProvided, amount0Min, amount1Min, lowerTick, upperTick)
if err != nil {
return CreatePositionData{}, err
}

positionId := k.getNextPositionIdAndIncrement(ctx)

if !hasPositions {
err := k.initializeInitialPositionForPool(ctx, pool, amount0Desired, amount1Desired)
if err != nil {
Expand Down Expand Up @@ -178,6 +188,13 @@ func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr
}
k.RecordTotalLiquidityIncrease(ctx, tokensAdded)

// Trigger after hook for CreatePosition.
// If no contract is set, this will be a no-op.
err = k.AfterCreatePosition(ctx, poolId, owner, tokensProvided, amount0Min, amount1Min, lowerTick, upperTick)
if err != nil {
return CreatePositionData{}, err
}

return CreatePositionData{
ID: positionId,
Amount0: updateData.Amount0,
Expand All @@ -203,6 +220,9 @@ func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr
// - if the position's underlying lock is not mature
// - if tick ranges are invalid
// - if attempts to withdraw an amount higher than originally provided in createPosition for a given range.
//
// BeforeWithdrawPosition hook is triggered after validation logic but before any state changes are made.
// AfterWithdrawPosition hook is triggered after state changes are complete if no errors have occurred.
func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, positionId uint64, requestedLiquidityAmountToWithdraw osmomath.Dec) (amtDenom0, amtDenom1 osmomath.Int, err error) {
position, err := k.GetPosition(ctx, positionId)
if err != nil {
Expand Down Expand Up @@ -243,6 +263,13 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
return osmomath.Int{}, osmomath.Int{}, types.InsufficientLiquidityError{Actual: requestedLiquidityAmountToWithdraw, Available: position.Liquidity}
}

// Trigger before hook for WithdrawPosition prior to mutating state.
// If no contract is set, this will be a no-op.
err = k.BeforeWithdrawPosition(ctx, position.PoolId, owner, positionId, requestedLiquidityAmountToWithdraw)
if err != nil {
return osmomath.Int{}, osmomath.Int{}, err
}

_, _, err = k.collectIncentives(ctx, owner, positionId)
if err != nil {
return osmomath.Int{}, osmomath.Int{}, err
Expand Down Expand Up @@ -331,6 +358,13 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
}
event.emit(ctx)

// Trigger after hook for WithdrawPosition.
// If no contract is set, this will be a no-op.
err = k.AfterWithdrawPosition(ctx, position.PoolId, owner, positionId, requestedLiquidityAmountToWithdraw)
if err != nil {
return osmomath.Int{}, osmomath.Int{}, err
}

return updateData.Amount0.Neg(), updateData.Amount1.Neg(), nil
}

Expand Down
1 change: 0 additions & 1 deletion x/concentrated-liquidity/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func (server msgServer) CreateConcentratedPool(goCtx context.Context, msg *clmod
return &clmodel.MsgCreateConcentratedPoolResponse{PoolID: poolId}, nil
}

// TODO: tests, including events
func (server msgServer) CreatePosition(goCtx context.Context, msg *types.MsgCreatePosition) (*types.MsgCreatePositionResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

Expand Down
54 changes: 13 additions & 41 deletions x/concentrated-liquidity/pool_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package concentrated_liquidity

import (
"encoding/json"
"fmt"

"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -12,16 +11,6 @@ import (
types "github.com/osmosis-labs/osmosis/v20/x/concentrated-liquidity/types"
)

// Helper function to generate before action prefix
func beforeActionPrefix(action string) string {
return fmt.Sprintf("before%s", action)
}

// Helper function to generate after action prefix
func afterActionPrefix(action string) string {
return fmt.Sprintf("after%s", action)
}

// --- Pool Hooks ---

// BeforeCreatePosition is a hook that is called before a position is created.
Expand All @@ -32,7 +21,7 @@ func (k Keeper) BeforeCreatePosition(ctx sdk.Context, poolId uint64, owner sdk.A
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, beforeActionPrefix(types.CreatePositionPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.CreatePositionPrefix))
}

// AfterCreatePosition is a hook that is called after a position is created.
Expand All @@ -43,29 +32,7 @@ func (k Keeper) AfterCreatePosition(ctx sdk.Context, poolId uint64, owner sdk.Ac
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, afterActionPrefix(types.CreatePositionPrefix))
}

// BeforeAddToPosition is a hook that is called before liquidity is added to a position.
func (k Keeper) BeforeAddToPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, positionId uint64, amount0Added osmomath.Int, amount1Added osmomath.Int, amount0Min osmomath.Int, amount1Min osmomath.Int) error {
// Build and marshal the message to be passed to the contract
msg := types.BeforeAddToPositionMsg{PoolId: poolId, Owner: owner, PositionId: positionId, Amount0Added: amount0Added, Amount1Added: amount1Added, Amount0Min: amount0Min, Amount1Min: amount1Min}
msgBz, err := json.Marshal(types.BeforeAddToPositionSudoMsg{BeforeAddToPosition: msg})
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, beforeActionPrefix(types.AddToPositionPrefix))
}

// AfterAddToPosition is a hook that is called after liquidity is added to a position.
func (k Keeper) AfterAddToPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, positionId uint64, amount0Added osmomath.Int, amount1Added osmomath.Int, amount0Min osmomath.Int, amount1Min osmomath.Int) error {
// Build and marshal the message to be passed to the contract
msg := types.AfterAddToPositionMsg{PoolId: poolId, Owner: owner, PositionId: positionId, Amount0Added: amount0Added, Amount1Added: amount1Added, Amount0Min: amount0Min, Amount1Min: amount1Min}
msgBz, err := json.Marshal(types.AfterAddToPositionSudoMsg{AfterAddToPosition: msg})
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, afterActionPrefix(types.AddToPositionPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.AfterActionPrefix(types.CreatePositionPrefix))
}

// BeforeWithdrawPosition is a hook that is called before liquidity is withdrawn from a position.
Expand All @@ -76,7 +43,7 @@ func (k Keeper) BeforeWithdrawPosition(ctx sdk.Context, poolId uint64, owner sdk
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, beforeActionPrefix(types.WithdrawPositionPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.WithdrawPositionPrefix))
}

// AfterWithdrawPosition is a hook that is called after liquidity is withdrawn from a position.
Expand All @@ -87,7 +54,7 @@ func (k Keeper) AfterWithdrawPosition(ctx sdk.Context, poolId uint64, owner sdk.
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, afterActionPrefix(types.WithdrawPositionPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.AfterActionPrefix(types.WithdrawPositionPrefix))
}

// BeforeSwapExactAmountIn is a hook that is called before a swap is executed (exact amount in).
Expand All @@ -98,7 +65,7 @@ func (k Keeper) BeforeSwapExactAmountIn(ctx sdk.Context, poolId uint64, sender s
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, beforeActionPrefix(types.SwapExactAmountInPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.SwapExactAmountInPrefix))
}

// AfterSwapExactAmountIn is a hook that is called after a swap is executed (exact amount in).
Expand All @@ -109,7 +76,7 @@ func (k Keeper) AfterSwapExactAmountIn(ctx sdk.Context, poolId uint64, sender sd
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, afterActionPrefix(types.SwapExactAmountInPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.AfterActionPrefix(types.SwapExactAmountInPrefix))
}

// BeforeSwapExactAmountOut is a hook that is called before a swap is executed (exact amount out).
Expand All @@ -120,7 +87,7 @@ func (k Keeper) BeforeSwapExactAmountOut(ctx sdk.Context, poolId uint64, sender
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, beforeActionPrefix(types.SwapExactAmountOutPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.SwapExactAmountOutPrefix))
}

// AfterSwapExactAmountOut is a hook that is called after a swap is executed (exact amount out).
Expand All @@ -131,7 +98,7 @@ func (k Keeper) AfterSwapExactAmountOut(ctx sdk.Context, poolId uint64, sender s
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, afterActionPrefix(types.SwapExactAmountOutPrefix))
return k.callPoolActionListener(ctx, msgBz, poolId, types.AfterActionPrefix(types.SwapExactAmountOutPrefix))
}

// callPoolActionListener processes and dispatches the passed in message to the contract corresponding to the hook
Expand Down Expand Up @@ -210,6 +177,11 @@ func (k Keeper) getPoolHookContract(ctx sdk.Context, poolId uint64, actionPrefix
func (k Keeper) setPoolHookContract(ctx sdk.Context, poolID uint64, actionPrefix string, cosmwasmAddress string) error {
store := k.getPoolHookPrefixStore(ctx, poolID)

validActionPrefixes := types.GetAllActionPrefixes()
if !osmoutils.Contains(validActionPrefixes, actionPrefix) {
return types.InvalidActionPrefixError{ActionPrefix: actionPrefix, ValidActions: validActionPrefixes}
}

// If cosmwasm address is nil, treat this as a delete operation for the stored address.
if cosmwasmAddress == "" {
deletePoolHookContract(store, actionPrefix)
Expand Down
Loading

0 comments on commit 059c5db

Please sign in to comment.