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 @@ -51,6 +51,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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require (
github.com/ory/dockertest/v3 v3.10.0
github.com/osmosis-labs/go-mutesting v0.0.0-20221208041716-b43bcd97b3b3
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20231014001935-1946419d44eb
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231017074304-84e27b5e2aad
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231109071700-0d4140edf903
github.com/osmosis-labs/osmosis/x/epochs v0.0.3-0.20231011004221-fd24b80f8366
github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.9-0.20231014001935-1946419d44eb
github.com/pkg/errors v0.9.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,8 @@ github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20231014001935-1946419d44eb h1
github.com/osmosis-labs/osmosis/osmomath v0.0.7-0.20231014001935-1946419d44eb/go.mod h1:jNZ952fypVNMzOsh31LAUS27JbF9naNJGtELxId6ZCg=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231017074304-84e27b5e2aad h1:UcQ/XLz0SqWMrA+BhgDXy9ukD4C+FlN4ULdazZmFOsE=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231017074304-84e27b5e2aad/go.mod h1:16AXMzbTLkYE5If5VLTA07fV9JNcLFwgf/VoW5sHrtU=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231109071700-0d4140edf903 h1:trHTdYMh8IeIsMkv0Fik94ClJV0FxWDv8KBgOyn6HvE=
github.com/osmosis-labs/osmosis/osmoutils v0.0.7-0.20231109071700-0d4140edf903/go.mod h1:16AXMzbTLkYE5If5VLTA07fV9JNcLFwgf/VoW5sHrtU=
github.com/osmosis-labs/osmosis/x/epochs v0.0.3-0.20231011004221-fd24b80f8366 h1:E6H0V3MKbSNwo1iXE9Kzatd2M02MgZpS5AiJ6CKK5us=
github.com/osmosis-labs/osmosis/x/epochs v0.0.3-0.20231011004221-fd24b80f8366/go.mod h1:vU0IHK5W38dqMeux3MkSaT3MZU6whAkx7vNuxv1IzeU=
github.com/osmosis-labs/osmosis/x/ibc-hooks v0.0.9-0.20231014001935-1946419d44eb h1:6lYLEiJERdD+QK925XYyHkvNyvQTghVFufMH5VAQLpg=
Expand Down
18 changes: 18 additions & 0 deletions osmoutils/coin_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package osmoutils
import (
"fmt"

wasmvmtypes "github.com/CosmWasm/wasmvm/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

Expand Down Expand Up @@ -114,3 +115,20 @@ func MergeCoinMaps[T comparable](currentEpochExpectedDistributionsOne map[T]sdk.
}
return newMap
}

// Convert sdk.Coins to wasmvmtypes.Coins
func CWCoinsFromSDKCoins(in sdk.Coins) wasmvmtypes.Coins {
var cwCoins wasmvmtypes.Coins
for _, coin := range in {
cwCoins = append(cwCoins, CWCoinFromSDKCoin(coin))
}
return cwCoins
}

// Convert sdk.Coin to wasmvmtypes.Coin
func CWCoinFromSDKCoin(in sdk.Coin) wasmvmtypes.Coin {
return wasmvmtypes.Coin{
Denom: in.GetDenom(),
Amount: in.Amount.String(),
}
}
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
100 changes: 99 additions & 1 deletion x/concentrated-liquidity/pool_hooks.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,106 @@
package concentrated_liquidity

import (
"encoding/json"

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

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/osmosis/osmoutils"
types "github.com/osmosis-labs/osmosis/v20/x/concentrated-liquidity/types"
)

// nolint: unused
// --- Pool Hooks ---

// BeforeCreatePosition is a hook that is called before a position is created.
func (k Keeper) BeforeCreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min osmomath.Int, amount1Min osmomath.Int, lowerTick int64, upperTick int64) error {
// Build and marshal the message to be passed to the contract
msg := types.BeforeCreatePositionMsg{PoolId: poolId, Owner: owner, TokensProvided: osmoutils.CWCoinsFromSDKCoins(tokensProvided), Amount0Min: amount0Min, Amount1Min: amount1Min, LowerTick: lowerTick, UpperTick: upperTick}
msgBz, err := json.Marshal(types.BeforeCreatePositionSudoMsg{BeforeCreatePosition: msg})
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.CreatePositionPrefix))
}

// AfterCreatePosition is a hook that is called after a position is created.
func (k Keeper) AfterCreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min osmomath.Int, amount1Min osmomath.Int, lowerTick int64, upperTick int64) error {
// Build and marshal the message to be passed to the contract
msg := types.AfterCreatePositionMsg{PoolId: poolId, Owner: owner, TokensProvided: osmoutils.CWCoinsFromSDKCoins(tokensProvided), Amount0Min: amount0Min, Amount1Min: amount1Min, LowerTick: lowerTick, UpperTick: upperTick}
msgBz, err := json.Marshal(types.AfterCreatePositionSudoMsg{AfterCreatePosition: msg})
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, types.AfterActionPrefix(types.CreatePositionPrefix))
}

// BeforeWithdrawPosition is a hook that is called before liquidity is withdrawn from a position.
func (k Keeper) BeforeWithdrawPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, positionId uint64, amountToWithdraw osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.BeforeWithdrawPositionMsg{PoolId: poolId, Owner: owner, PositionId: positionId, AmountToWithdraw: amountToWithdraw}
msgBz, err := json.Marshal(types.BeforeWithdrawPositionSudoMsg{BeforeWithdrawPosition: msg})
if err != nil {
return err
}
return k.callPoolActionListener(ctx, msgBz, poolId, types.BeforeActionPrefix(types.WithdrawPositionPrefix))
}

// AfterWithdrawPosition is a hook that is called after liquidity is withdrawn from a position.
func (k Keeper) AfterWithdrawPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, positionId uint64, amountToWithdraw osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.AfterWithdrawPositionMsg{PoolId: poolId, Owner: owner, PositionId: positionId, AmountToWithdraw: amountToWithdraw}
msgBz, err := json.Marshal(types.AfterWithdrawPositionSudoMsg{AfterWithdrawPosition: msg})
if err != nil {
return err
}
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).
func (k Keeper) BeforeSwapExactAmountIn(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, tokenIn sdk.Coin, tokenOutDenom string, tokenOutMinAmount osmomath.Int, spreadFactor osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.BeforeSwapExactAmountInMsg{PoolId: poolId, Sender: sender, TokenIn: osmoutils.CWCoinFromSDKCoin(tokenIn), TokenOutDenom: tokenOutDenom, TokenOutMinAmount: tokenOutMinAmount, SpreadFactor: spreadFactor}
msgBz, err := json.Marshal(types.BeforeSwapExactAmountInSudoMsg{BeforeSwapExactAmountIn: msg})
if err != nil {
return err
}
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).
func (k Keeper) AfterSwapExactAmountIn(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, tokenIn sdk.Coin, tokenOutDenom string, tokenOutMinAmount osmomath.Int, spreadFactor osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.AfterSwapExactAmountInMsg{PoolId: poolId, Sender: sender, TokenIn: osmoutils.CWCoinFromSDKCoin(tokenIn), TokenOutDenom: tokenOutDenom, TokenOutMinAmount: tokenOutMinAmount, SpreadFactor: spreadFactor}
msgBz, err := json.Marshal(types.AfterSwapExactAmountInSudoMsg{AfterSwapExactAmountIn: msg})
if err != nil {
return err
}
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).
func (k Keeper) BeforeSwapExactAmountOut(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, tokenInDenom string, tokenInMaxAmount osmomath.Int, tokenOut sdk.Coin, spreadFactor osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.BeforeSwapExactAmountOutMsg{PoolId: poolId, Sender: sender, TokenInDenom: tokenInDenom, TokenInMaxAmount: tokenInMaxAmount, TokenOut: osmoutils.CWCoinFromSDKCoin(tokenOut), SpreadFactor: spreadFactor}
msgBz, err := json.Marshal(types.BeforeSwapExactAmountOutSudoMsg{BeforeSwapExactAmountOut: msg})
if err != nil {
return err
}
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).
func (k Keeper) AfterSwapExactAmountOut(ctx sdk.Context, poolId uint64, sender sdk.AccAddress, tokenInDenom string, tokenInMaxAmount osmomath.Int, tokenOut sdk.Coin, spreadFactor osmomath.Dec) error {
// Build and marshal the message to be passed to the contract
msg := types.AfterSwapExactAmountOutMsg{PoolId: poolId, Sender: sender, TokenInDenom: tokenInDenom, TokenInMaxAmount: tokenInMaxAmount, TokenOut: osmoutils.CWCoinFromSDKCoin(tokenOut), SpreadFactor: spreadFactor}
msgBz, err := json.Marshal(types.AfterSwapExactAmountOutSudoMsg{AfterSwapExactAmountOut: msg})
if err != nil {
return err
}
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
// defined by the given pool ID and action prefix (e.g. pool Id: 1, action prefix: "beforeSwap").
//
Expand Down Expand Up @@ -84,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