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: CL hooks]: Add CL hooks into core CL logic and test hook-specific behavior #6859

Merged
merged 13 commits into from
Nov 22, 2023
Merged
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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: this was moved below the hasPositions check to ensure the before hook was triggered after validation logic but before any state changes (getNextPositionIdAndIncrement mutates state)

// 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

AddToPosition hooks were removed since the function just calls WithdrawPosition then CreatePosition under the hood. This makes it tricky to separate out AddToPosition hooks from the other two and imo unnecessary complexity that would increase the surface for circumventing hooks.

Opted to remove and keep hooks on the most primitive actions.

// 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