-
Notifications
You must be signed in to change notification settings - Fork 0
/
lp.go
285 lines (243 loc) · 12.8 KB
/
lp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package concentrated_liquidity
import (
"errors"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/merlinslair/merlin/x/concentrated-liquidity/internal/math"
types "github.com/merlinslair/merlin/x/concentrated-liquidity/types"
)
// CreatePosition creates a concentrated liquidity position in range between lowerTick and upperTick
// in a given `PoolId with the desired amount of each token. Since LPs are only allowed to provide
// liquidity proportional to the existing reserves, the actual amount of tokens used might differ from requested.
// As a result, LPs may also provide the minimum amount of each token to be used so that the system fails
// to create position if the desired amounts cannot be satisfied.
// On success, returns an actual amount of each token used and liquidity created.
// Returns error if:
// - the provided ticks are out of range / invalid
// - the pool provided does not exist
// - 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
func (k Keeper) CreatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, amount0Desired, amount1Desired, amount0Min, amount1Min sdk.Int, lowerTick, upperTick int64, frozenUntil time.Time) (sdk.Int, sdk.Int, sdk.Dec, error) {
// Retrieve the pool associated with the given pool ID.
pool, err := k.getPoolById(ctx, poolId)
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
// Check if the provided tick range is valid according to the pool's tick spacing and module parameters.
if err := validateTickRangeIsValid(pool.GetTickSpacing(), pool.GetPrecisionFactorAtPriceOne(), lowerTick, upperTick); err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
// Transform the provided ticks into their corresponding sqrtPrices.
sqrtPriceLowerTick, sqrtPriceUpperTick, err := math.TicksToSqrtPrice(lowerTick, upperTick, pool.GetPrecisionFactorAtPriceOne())
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
// Create a cache context for the current transaction.
// This allows us to make changes to the context without persisting it until later.
// We only write the cache context (i.e. persist the changes) if the actual amounts returned
// are greater than the given minimum amounts.
cacheCtx, writeCacheCtx := ctx.CacheContext()
initialSqrtPrice := pool.GetCurrentSqrtPrice()
initialTick := pool.GetCurrentTick()
// If the current square root price and current tick are zero, then this is the first position to be created for this pool.
// In this case, we calculate the square root price and current tick based on the inputs of this position.
if k.isInitialPositionForPool(initialSqrtPrice, initialTick) {
err := k.initializeInitialPositionForPool(cacheCtx, pool, amount0Desired, amount1Desired)
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
}
// Calculate the amount of liquidity that will be added to the pool by creating this position.
liquidityDelta := math.GetLiquidityFromAmounts(pool.GetCurrentSqrtPrice(), sqrtPriceLowerTick, sqrtPriceUpperTick, amount0Desired, amount1Desired)
if liquidityDelta.IsZero() {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, errors.New("liquidityDelta calculated equals zero")
}
// If this is a new position, initialize the fee accumulator for the position.
positions, err := k.getAllPositionsWithVaryingFreezeTimes(ctx, poolId, owner, lowerTick, upperTick)
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
if len(positions) == 0 {
if err := k.initializeFeeAccumulatorPosition(cacheCtx, poolId, owner, lowerTick, upperTick); err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
}
// Update the position in the pool based on the provided tick range and liquidity delta.
actualAmount0, actualAmount1, err := k.updatePosition(cacheCtx, poolId, owner, lowerTick, upperTick, liquidityDelta, frozenUntil)
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
// Check if the actual amounts of tokens 0 and 1 are greater than or equal to the given minimum amounts.
if actualAmount0.LT(amount0Min) {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, types.InsufficientLiquidityCreatedError{Actual: actualAmount0, Minimum: amount0Min, IsTokenZero: true}
}
if actualAmount1.LT(amount1Min) {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, types.InsufficientLiquidityCreatedError{Actual: actualAmount1, Minimum: amount1Min}
}
// Transfer the actual amounts of tokens 0 and 1 from the position owner to the pool.
err = k.sendCoinsBetweenPoolAndUser(cacheCtx, pool.GetToken0(), pool.GetToken1(), actualAmount0, actualAmount1, owner, pool.GetAddress())
if err != nil {
return sdk.Int{}, sdk.Int{}, sdk.Dec{}, err
}
// Persist the changes made to the cache context if the actual amounts of tokens 0 and 1 are greater than or equal to the given minimum amounts.
writeCacheCtx()
return actualAmount0, actualAmount1, liquidityDelta, nil
}
// 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.
// Returns error if
// - there is no position in the given tick ranges
// - if tick ranges are invalid
// - if attempts to withdraw an amount higher than originally provided in createPosition for a given range.
func (k Keeper) WithdrawPosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, lowerTick, upperTick int64, frozenUntil time.Time, requestedLiquidityAmountToWithdraw sdk.Dec) (amtDenom0, amtDenom1 sdk.Int, err error) {
// Retrieve the pool associated with the given pool ID.
pool, err := k.getPoolById(ctx, poolId)
if err != nil {
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(), pool.GetPrecisionFactorAtPriceOne(), lowerTick, upperTick); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// Retrieve the position in the pool for the provided owner and tick range.
position, err := k.GetPosition(ctx, poolId, owner, lowerTick, upperTick, frozenUntil)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// Check if position is still frozen
if position.FrozenUntil.After(ctx.BlockTime()) {
return sdk.Int{}, sdk.Int{}, types.PositionStillFrozenError{FrozenUntil: position.FrozenUntil}
}
// 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.
availableLiquidity := position.Liquidity
if requestedLiquidityAmountToWithdraw.GT(availableLiquidity) {
return sdk.Int{}, sdk.Int{}, types.InsufficientLiquidityError{Actual: requestedLiquidityAmountToWithdraw, Available: availableLiquidity}
}
// Calculate the change in liquidity for the pool based on the requested amount to withdraw.
// This amount is negative because that liquidity is being withdrawn from the pool.
liquidityDelta := requestedLiquidityAmountToWithdraw.Neg()
// Update the position in the pool based on the provided tick range and liquidity delta.
actualAmount0, actualAmount1, err := k.updatePosition(ctx, poolId, owner, lowerTick, upperTick, liquidityDelta, frozenUntil)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// Transfer the actual amounts of tokens 0 and 1 from the pool to the position owner.
err = k.sendCoinsBetweenPoolAndUser(ctx, pool.GetToken0(), pool.GetToken1(), actualAmount0.Abs(), actualAmount1.Abs(), pool.GetAddress(), owner)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// If the requested liquidity amount to withdraw is equal to the available liquidity, delete the position from state.
// Ensure we collect any outstanding fees prior to deleting the position from state
if requestedLiquidityAmountToWithdraw.Equal(availableLiquidity) {
if _, err := k.collectFees(ctx, poolId, owner, lowerTick, upperTick); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
if err := k.deletePosition(ctx, poolId, owner, lowerTick, upperTick, frozenUntil); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
}
return actualAmount0.Neg(), actualAmount1.Neg(), nil
}
// updatePosition updates the position in the given pool id and in the given tick range and liquidityAmount.
// Negative liquidityDelta implies withdrawing liquidity.
// Positive liquidityDelta implies adding liquidity.
// Updates ticks and pool liquidity. Returns how much of each token is either added or removed.
// Negative returned amounts imply that tokens are removed from the pool.
// Positive returned amounts imply that tokens are added to the pool.
func (k Keeper) updatePosition(ctx sdk.Context, poolId uint64, owner sdk.AccAddress, lowerTick, upperTick int64, liquidityDelta sdk.Dec, frozenUntil time.Time) (sdk.Int, sdk.Int, error) {
// update tickInfo state
// TODO: come back to sdk.Int vs sdk.Dec state & truncation
err := k.initOrUpdateTick(ctx, poolId, lowerTick, liquidityDelta, false)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// TODO: come back to sdk.Int vs sdk.Dec state & truncation
err = k.initOrUpdateTick(ctx, poolId, upperTick, liquidityDelta, true)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// update position state
// TODO: come back to sdk.Int vs sdk.Dec state & truncation
err = k.initOrUpdatePosition(ctx, poolId, owner, lowerTick, upperTick, liquidityDelta, frozenUntil)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// now calculate amount for token0 and token1
pool, err := k.getPoolById(ctx, poolId)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// Transform the provided ticks into their corresponding sqrtPrices.
sqrtPriceLowerTick, sqrtPriceUpperTick, err := math.TicksToSqrtPrice(lowerTick, upperTick, pool.GetPrecisionFactorAtPriceOne())
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
actualAmount0, actualAmount1 := pool.CalcActualAmounts(ctx, lowerTick, upperTick, sqrtPriceLowerTick, sqrtPriceUpperTick, liquidityDelta)
if err != nil {
return sdk.Int{}, sdk.Int{}, err
}
pool.UpdateLiquidityIfActivePosition(ctx, lowerTick, upperTick, liquidityDelta)
if err := k.setPool(ctx, pool); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// TODO: test https://github.com/merlinslair/merlin/issues/3997
if err := k.updateFeeAccumulatorPosition(ctx, poolId, owner, liquidityDelta, lowerTick, upperTick); err != nil {
return sdk.Int{}, sdk.Int{}, err
}
// The returned amounts are rounded down to avoid returning more to clients than they actually deposited.
return actualAmount0.TruncateInt(), actualAmount1.TruncateInt(), nil
}
// sendCoinsBetweenPoolAndUser takes the amounts calculated from a join/exit position and executes the send between pool and user
func (k Keeper) sendCoinsBetweenPoolAndUser(ctx sdk.Context, denom0, denom1 string, amount0, amount1 sdk.Int, sender, receiver sdk.AccAddress) error {
if amount0.IsNegative() {
return fmt.Errorf("amount0 is negative: %s", amount0)
}
if amount1.IsNegative() {
return fmt.Errorf("amount1 is negative: %s", amount1)
}
finalCoinsToSend := sdk.NewCoins(sdk.NewCoin(denom1, amount1), sdk.NewCoin(denom0, amount0))
err := k.bankKeeper.SendCoins(ctx, sender, receiver, finalCoinsToSend)
if err != nil {
return err
}
return nil
}
// isInitialPositionForPool checks if the initial sqrtPrice and initial tick are equal to zero.
// If so, this is the first position to be created for this pool, and we return true.
// If not, we return false.
func (k Keeper) isInitialPositionForPool(initialSqrtPrice sdk.Dec, initialTick sdk.Int) bool {
if initialSqrtPrice.Equal(sdk.ZeroDec()) && initialTick.Equal(sdk.ZeroInt()) {
return true
}
return false
}
// createInitialPosition ensures that the first position created on this pool includes both asset0 and asset1
// This is required so we can set the pool's sqrtPrice and calculate it's initial tick from this
func (k Keeper) initializeInitialPositionForPool(ctx sdk.Context, pool types.ConcentratedPoolExtension, amount0Desired, amount1Desired sdk.Int) error {
// Check that the position includes some amount of both asset0 and asset1
if !amount0Desired.GT(sdk.ZeroInt()) || !amount1Desired.GT(sdk.ZeroInt()) {
return types.InitialLiquidityZeroError{Amount0: amount0Desired, Amount1: amount1Desired}
}
// Calculate the spot price and sqrt price from the amount provided
initialSpotPrice := amount1Desired.ToDec().Quo(amount0Desired.ToDec())
initialSqrtPrice, err := initialSpotPrice.ApproxSqrt()
if err != nil {
return err
}
// Calculate the initial tick from the initial spot price
initialTick, err := math.PriceToTick(initialSpotPrice, pool.GetPrecisionFactorAtPriceOne())
if err != nil {
return err
}
// Set the pool's current sqrt price and current tick to the above calculated values
pool.SetCurrentSqrtPrice(initialSqrtPrice)
pool.SetCurrentTick(initialTick)
err = k.setPool(ctx, pool)
if err != nil {
return err
}
return nil
}