Skip to content

Commit

Permalink
[CL Message Audit]: MsgWithdrawPosition (#5135)
Browse files Browse the repository at this point in the history
* WIP

* Add test cases

* Uncomment tests

* Uncomment more tests

* Add lock test

* And uncomment test

* Update x/concentrated-liquidity/incentives.go

Co-authored-by: Roman <[email protected]>

* Romans feedback

* Update x/concentrated-liquidity/lp.go

Co-authored-by: Adam Tucker <[email protected]>

* Update x/concentrated-liquidity/lp_test.go

Co-authored-by: Adam Tucker <[email protected]>

* Adams review

---------

Co-authored-by: Roman <[email protected]>
Co-authored-by: Adam Tucker <[email protected]>
  • Loading branch information
3 people authored May 17, 2023
1 parent db52beb commit d7bb061
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 94 deletions.
1 change: 1 addition & 0 deletions osmoutils/accum/accum.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ func (accum AccumulatorObject) GetValue() sdk.DecCoins {
// ClaimRewards claims the rewards for the given address, and returns the amount of rewards claimed.
// Upon claiming the rewards, the position at the current address is reset to have no
// unclaimed rewards. The position's accumulator is also set to the current accumulator value.
// The position state is removed if the position shares is equal to zero.
//
// Returns error if
// - no position exists for the given address
Expand Down
12 changes: 8 additions & 4 deletions x/concentrated-liquidity/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,12 @@ func ValidateTickRangeIsValid(tickSpacing uint64, lowerTick int64, upperTick int
return validateTickRangeIsValid(tickSpacing, lowerTick, upperTick)
}

func PreparePositionAccumulator(feeAccumulator accum.AccumulatorObject, positionKey string, feeGrowthOutside sdk.DecCoins) error {
return preparePositionAccumulator(feeAccumulator, positionKey, feeGrowthOutside)
func UpdatePosValueToInitValuePlusGrowthOutside(feeAccumulator accum.AccumulatorObject, positionKey string, feeGrowthOutside sdk.DecCoins) error {
return updatePositionToInitValuePlusGrowthOutside(feeAccumulator, positionKey, feeGrowthOutside)
}

func UpdatePositionToInitValuePlusGrowthOutside(accumulator accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) error {
return updatePositionToInitValuePlusGrowthOutside(accumulator, positionKey, growthOutside)
}

func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, tokensProvided sdk.Coins, amount0Min, amount1Min sdk.Int, lowerTick, upperTick int64) (positionId uint64, actualAmount0 sdk.Int, actualAmount1 sdk.Int, liquidityDelta sdk.Dec, joinTime time.Time, lowerTickResult int64, upperTickResult int64, err error) {
Expand Down Expand Up @@ -280,8 +284,8 @@ func GetUptimeTrackerValues(uptimeTrackers []model.UptimeTracker) []sdk.DecCoins
return getUptimeTrackerValues(uptimeTrackers)
}

func PrepareAccumAndClaimRewards(accum accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) (sdk.Coins, sdk.DecCoins, error) {
return prepareAccumAndClaimRewards(accum, positionKey, growthOutside)
func UpdateAccumAndClaimRewards(accum accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) (sdk.Coins, sdk.DecCoins, error) {
return updateAccumAndClaimRewards(accum, positionKey, growthOutside)
}

func (k Keeper) ClaimAllIncentivesForPosition(ctx sdk.Context, positionId uint64) (sdk.Coins, sdk.Coins, error) {
Expand Down
9 changes: 4 additions & 5 deletions x/concentrated-liquidity/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (k Keeper) initOrUpdatePositionFeeAccumulator(ctx sdk.Context, poolId uint6
// At time t, we track fee growth inside from 0 to t.
// Then, the update happens at time t + 1. The call below makes the position's
// accumulator to be "fee growth inside from 0 to t + fee growth outside from 0 to t + 1".
err = preparePositionAccumulator(feeAccumulator, positionKey, feeGrowthOutside)
err = updatePositionToInitValuePlusGrowthOutside(feeAccumulator, positionKey, feeGrowthOutside)
if err != nil {
return err
}
Expand Down Expand Up @@ -275,7 +275,7 @@ func (k Keeper) prepareClaimableFees(ctx sdk.Context, positionId uint64) (sdk.Co
}

// Claim rewards, set the unclaimed rewards to zero, and update the position's accumulator value to reflect the current accumulator value.
feesClaimed, _, err := prepareAccumAndClaimRewards(feeAccumulator, positionKey, feeGrowthOutside)
feesClaimed, _, err := updateAccumAndClaimRewards(feeAccumulator, positionKey, feeGrowthOutside)
if err != nil {
return nil, err
}
Expand All @@ -295,12 +295,11 @@ func calculateFeeGrowth(targetTick int64, ticksFeeGrowthOppositeDirectionOfLastT
return ticksFeeGrowthOppositeDirectionOfLastTraversal
}

// preparePositionAccumulator is called prior to updating unclaimed rewards,
// updatePositionToInitValuePlusGrowthOutside is called prior to updating unclaimed rewards,
// as we must set the position's accumulator value to the sum of
// - the fee/uptime growth inside at position creation time (position.InitAccumValue)
// - fee/uptime growth outside at the current block time (feeGrowthOutside/uptimeGrowthOutside)
// CONTRACT: position accumulator value prior to this call is equal to the growth inside the position at the time of last update.
func preparePositionAccumulator(accumulator accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) error {
func updatePositionToInitValuePlusGrowthOutside(accumulator accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) error {
position, err := accum.GetPosition(accumulator, positionKey)
if err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions x/concentrated-liquidity/fees_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1335,7 +1335,7 @@ func (s *KeeperTestSuite) TestInitOrUpdateFeeAccumulatorPosition_UpdatingPositio
}
}

func (s *KeeperTestSuite) TestPreparePositionAccumulator() {
func (s *KeeperTestSuite) TestUpdatePosValueToInitValuePlusGrowthOutside() {
validPositionKey := types.KeyFeePositionAccumulator(1)
invalidPositionKey := types.KeyFeePositionAccumulator(2)
tests := []struct {
Expand Down Expand Up @@ -1382,7 +1382,7 @@ func (s *KeeperTestSuite) TestPreparePositionAccumulator() {
}

// System under test.
err = cl.PreparePositionAccumulator(poolFeeAccumulator, positionKey, tc.feeGrowthOutside)
err = cl.UpdatePosValueToInitValuePlusGrowthOutside(poolFeeAccumulator, positionKey, tc.feeGrowthOutside)

if tc.expectError != nil {
s.Require().Error(err)
Expand Down
13 changes: 7 additions & 6 deletions x/concentrated-liquidity/incentives.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ func (k Keeper) initOrUpdatePositionUptimeAccumulators(ctx sdk.Context, poolId u
}
} else {
// Prep accum since we claim rewards first under the hood before any update (otherwise we would overpay)
err = preparePositionAccumulator(curUptimeAccum, positionName, globalUptimeGrowthOutsideRange[uptimeIndex])
err = updatePositionToInitValuePlusGrowthOutside(curUptimeAccum, positionName, globalUptimeGrowthOutsideRange[uptimeIndex])
if err != nil {
return err
}
Expand All @@ -694,22 +694,23 @@ func (k Keeper) initOrUpdatePositionUptimeAccumulators(ctx sdk.Context, poolId u
return nil
}

// prepareAccumAndClaimRewards claims and returns the rewards that `positionKey` is entitled to, updating the accumulator's value before
// updateAccumAndClaimRewards claims and returns the rewards that `positionKey` is entitled to, updating the accumulator's value before
// and after claiming to ensure that rewards are never overdistributed.
// CONTRACT: position accumulator value prior to this call is equal to the growth inside the position at the time of last update.
// Returns error if:
// - fails to prepare position accumulator
// - fails to claim rewards
// - fails to check if position record exists
// - fails to update position accumulator with the current growth inside the position
func prepareAccumAndClaimRewards(accum accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) (sdk.Coins, sdk.DecCoins, error) {
func updateAccumAndClaimRewards(accum accum.AccumulatorObject, positionKey string, growthOutside sdk.DecCoins) (sdk.Coins, sdk.DecCoins, error) {
// Set the position's accumulator value to it's initial value at creation time plus the growth outside at this moment.
err := preparePositionAccumulator(accum, positionKey, growthOutside)
err := updatePositionToInitValuePlusGrowthOutside(accum, positionKey, growthOutside)
if err != nil {
return sdk.Coins{}, sdk.DecCoins{}, err
}

// Claim rewards, set the unclaimed rewards to zero, and update the position's accumulator value to reflect the current accumulator value.
// Removes the position state from accum if remaining liquidity is zero for the position.
incentivesClaimedCurrAccum, dust, err := accum.ClaimRewards(positionKey)
if err != nil {
return sdk.Coins{}, sdk.DecCoins{}, err
Expand Down Expand Up @@ -747,7 +748,7 @@ func moveRewardsToNewPositionAndDeleteOldAcc(ctx sdk.Context, accum accum.Accumu
return types.ModifySamePositionAccumulatorError{PositionAccName: oldPositionName}
}

if err := preparePositionAccumulator(accum, oldPositionName, growthOutside); err != nil {
if err := updatePositionToInitValuePlusGrowthOutside(accum, oldPositionName, growthOutside); err != nil {
return err
}

Expand Down Expand Up @@ -826,7 +827,7 @@ func (k Keeper) claimAllIncentivesForPosition(ctx sdk.Context, positionId uint64

// If the accumulator contains the position, claim the position's incentives.
if hasPosition {
collectedIncentivesForUptime, dust, err := prepareAccumAndClaimRewards(uptimeAccum, positionName, uptimeGrowthOutside[uptimeIndex])
collectedIncentivesForUptime, dust, err := updateAccumAndClaimRewards(uptimeAccum, positionName, uptimeGrowthOutside[uptimeIndex])
if err != nil {
return sdk.Coins{}, sdk.Coins{}, err
}
Expand Down
4 changes: 2 additions & 2 deletions x/concentrated-liquidity/incentives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3259,7 +3259,7 @@ func (s *KeeperTestSuite) TestCreateIncentive() {
}
}

func (s *KeeperTestSuite) TestPrepareAccumAndClaimRewards() {
func (s *KeeperTestSuite) TestUpdateAccumAndClaimRewards() {
validPositionKey := types.KeyFeePositionAccumulator(1)
invalidPositionKey := types.KeyFeePositionAccumulator(2)
tests := map[string]struct {
Expand Down Expand Up @@ -3307,7 +3307,7 @@ func (s *KeeperTestSuite) TestPrepareAccumAndClaimRewards() {
poolFeeAccumulator.AddToAccumulator(tc.growthOutside.Add(tc.growthInside...))

// System under test.
amountClaimed, _, err := cl.PrepareAccumAndClaimRewards(poolFeeAccumulator, positionKey, tc.growthOutside)
amountClaimed, _, err := cl.UpdateAccumAndClaimRewards(poolFeeAccumulator, positionKey, tc.growthOutside)

if tc.expectError != nil {
s.Require().Error(err)
Expand Down
31 changes: 17 additions & 14 deletions x/concentrated-liquidity/lp.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,13 @@ func (k Keeper) createPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr

// WithdrawPosition attempts to withdraw liquidityAmount from a position with the given pool id in the given tick range.
// On success, returns a positive amount of each token withdrawn.
// If we are attempting to withdraw all liquidity available in the position, we also collect fees and incentives for the position.
// When the last position within a pool is removed, this function calls an AfterLastPoolPosistionRemoved listener
// Currently, it creates twap records. Assumming that pool had all liqudity drained and then re-initialized,
// the whole twap state is completely reset. This is because when there is no liquidity in pool, spot price
// is undefined.
// Additionally, when the last position is removed by calling this method, the current sqrt price and current
// tick are set to zero.
// tick of the pool are set to zero.
// Returns error if
// - the provided owner does not own the position being withdrawn
// - there is no position in the given tick ranges
Expand All @@ -150,14 +151,20 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
return sdk.Int{}, sdk.Int{}, types.NotPositionOwnerError{PositionId: positionId, Address: owner.String()}
}

// Defense in depth, requestedLiquidityAmountToWithdraw should always be a positive value.
if requestedLiquidityAmountToWithdraw.IsNegative() {
return sdk.Int{}, sdk.Int{}, types.InsufficientLiquidityError{Actual: requestedLiquidityAmountToWithdraw, Available: position.Liquidity}
}

// If underlying lock exists in state, validate unlocked conditions are met before withdrawing liquidity.
// If unlocked conditions are met, remove the link between the position and the underlying lock.
// If the underlying lock for the position has been matured, remove the link between the position and the underlying lock.
positionHasActiveUnderlyingLock, lockId, err := k.positionHasActiveUnderlyingLockAndUpdate(ctx, positionId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}

// If an underlying lock for the position exists, and the lock is not mature, return error.
if positionHasActiveUnderlyingLock {
// Lock is not mature, return error.
return sdk.Int{}, sdk.Int{}, types.LockNotMatureError{PositionId: position.PositionId, LockId: lockId}
}

Expand All @@ -167,13 +174,8 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
return sdk.Int{}, sdk.Int{}, err
}

// Check if the provided tick range is valid according to the pool's tick spacing and module parameters.
if err := validateTickRangeIsValid(pool.GetTickSpacing(), position.LowerTick, position.UpperTick); err != nil {
return sdk.Int{}, sdk.Int{}, err
}

// Retrieve the position in the pool for the provided owner and tick range.
availableLiquidity, err := k.GetPositionLiquidity(ctx, positionId)
positionLiquidity, err := k.GetPositionLiquidity(ctx, positionId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
Expand All @@ -185,8 +187,8 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position

// Check if the requested liquidity amount to withdraw is less than or equal to the available liquidity for the position.
// If it is greater than the available liquidity, return an error.
if requestedLiquidityAmountToWithdraw.GT(availableLiquidity) {
return sdk.Int{}, sdk.Int{}, types.InsufficientLiquidityError{Actual: requestedLiquidityAmountToWithdraw, Available: availableLiquidity}
if requestedLiquidityAmountToWithdraw.GT(positionLiquidity) {
return sdk.Int{}, sdk.Int{}, types.InsufficientLiquidityError{Actual: requestedLiquidityAmountToWithdraw, Available: positionLiquidity}
}

// Calculate the change in liquidity for the pool based on the requested amount to withdraw.
Expand All @@ -208,7 +210,7 @@ func (k Keeper) WithdrawPosition(ctx sdk.Context, owner sdk.AccAddress, position
// If the requested liquidity amount to withdraw is equal to the available liquidity, delete the position from state.
// Ensure we collect any outstanding fees and incentives prior to deleting the position from state. This claiming
// process also clears position records from fee and incentive accumulators.
if requestedLiquidityAmountToWithdraw.Equal(availableLiquidity) {
if requestedLiquidityAmountToWithdraw.Equal(positionLiquidity) {
if _, err := k.collectFees(ctx, owner, positionId); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
Expand Down Expand Up @@ -343,12 +345,13 @@ func (k Keeper) UpdatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddr

currentTick := pool.GetCurrentTick().Int64()

// update tickInfo state for lower tick and upper tick
// update lower tickInfo state
err = k.initOrUpdateTick(ctx, poolId, currentTick, lowerTick, liquidityDelta, false)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}

// update upper tickInfo state
err = k.initOrUpdateTick(ctx, poolId, currentTick, upperTick, liquidityDelta, true)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
Expand Down Expand Up @@ -445,7 +448,7 @@ func (k Keeper) initializeInitialPositionForPool(ctx sdk.Context, pool types.Con
return nil
}

// uninitializePool uninitializes a pool if it has no liquidity.
// uninitializePool reinitializes a pool if it has no liquidity.
// It does so by setting the current square root price and tick to zero.
// This is necessary for the twap to correctly detect a spot price error
// when there is no liquidity in the pool.
Expand Down
Loading

0 comments on commit d7bb061

Please sign in to comment.