-
Notifications
You must be signed in to change notification settings - Fork 589
/
README.md
1949 lines (1392 loc) · 85.7 KB
/
README.md
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Concentrated Liquidity
## Background
Concentrated liquidity is a novel Automated Market Maker (AMM) design introduced
by Uniswap that allows for more efficient use of capital. The improvement is
achieved by providing liquidity in specific price ranges chosen by the user.
For instance, a pool with stablecoin pairs like USDC/USDT has a spot price that
should always be trading near 1. As a result, Liquidity Providers (LPs) can
focus their capital in a small range around 1, rather than the full range from 0
to infinity. This approach leads to an average of 200-300x higher capital
efficiency. Moreover, traders benefit from lower price impact because the pool
incentivizes greater depth around the current price.
Concentrated liquidity also opens up new opportunities for providing liquidity
rewards to desired strategies. For example, it's possible to incentivize LPs
based on their position's proximity to the current price and the time spent
within that position. This design also allows for a new "range order" type,
similar to a limit order with order-books.
## Architecture
The traditional Balancer AMM relies on the following curve that tracks current reserves:
$$xy = k$$
This formula allows for distributing liquidity along the $xy=k$ curve and across
the entire price range of (0, ∞).
With the new architecture, we introduce the concept of a `position` that allows
users to concentrate liquidity within a fixed range. A position only needs to
maintain enough reserves to satisfy trading within this range. Consequently,
it functions as the traditional `xy = k` within that range.
In the new architecture, real reserves are described by the following formula:
$$(x + L / \sqrt P_u)(y + L \sqrt P_l) = L^2$$
Where `P_l` is the lower tick, `P_u` is the upper tick, and `L` is the amount
of liquidity provided, $$L = \sqrt k$$
This formula stems from the original $xy = k$ but with a limited range. In the
traditional design, a pool's `x` and `y` tokens are tracked directly. However,
with the concentrated design, we only track $L$ and $\sqrt P$, which can be
calculated with:
$$L = \sqrt {xy}$$
$$\sqrt P = \sqrt {y / x}$$
By rearranging the above, we obtain the following formulas to track virtual reserves:
$$x = L / \sqrt P$$
$$y = L \sqrt P$$
Note the square root around price. By tracking it this way, we can utilize the
following core property of the architecture:
$$L = \Delta y / \Delta \sqrt P$$
Since only one of the following changes at a time:
- $L$: When an LP adds or removes liquidity
- $\sqrt P$: When a trader swaps
We can use the above relationship to calculate the outcome of swaps as well as
pool joins that mint shares.
Conversely, we calculate liquidity from the other token in the pool:
$$\Delta x = \Delta \frac {1}{\sqrt P} L$$
Overall, the architecture's goal is to enable LPs to provide concentrated
liquidity within a specific range while maintaining high capital efficiency.
## Ticks
### Context
In Uniswap V3, discrete points (called ticks) are used when providing liquidity
in a concentrated liquidity pool.
The price [p] corresponding to a tick [t] is defined by the equation:
$$ p(t) = 1.0001^t $$
This results in a .01% difference between adjacent tick prices. This does not,
however, allow for control over the specific prices that the ticks correspond
to. For example, if a user wants to make a limit order at the $17,100.50 price point,
they would have to interact with either tick 97473 (corresponding to price
$17,099.60) or tick 97474 (price $17101.30).
Since we know what range a pair will generally trade in, how can we provide more
granularity at that range and provide a more optimal price range between ticks
instead of the "one-size-fits-all" approach explained above?
### Geometric Tick Spacing with Additive Ranges
In Osmosis' implementation of concentrated liquidity, we will instead make use
of geometric tick spacing with additive ranges.
We start by defining an exponent for the precision factor of each incremental
tick starting at the spot price of one. This is referred to as $exponentAtPriceOne$.
In the current design, we hardcode $exponentAtPriceOne$ as -6. When used with a
tick spacing of 100, this effectively acts as an $exponentAtPriceOne$ of -4,
since only every 100 ticks are able to be initialized.
When $exponentAtPriceOne = -6$ (and tick spacing is 100), each tick starting at
0 and ending at the first factor of 10 will represents a spot price increase of 0.0001:
- $tick_{0} = 1$
- $tick_{100} = 1.0001$
- $tick_{200} = 1.0002$
- $tick_{300} = 1.0003$
This continues until the pool reaches a spot price of 10. At this point, since
the pool has increased by a factor of 10, the $exponentAtCurrentTick$ increases
from -4 to -3 (decreasing the incremental precision), and the ticks will
increase as follows:
- $tick_{8999900} = 9.9999$
- $tick_{9000000} = 10.000$
- $tick_{9000100} = 10.001$
- $tick_{9000200} = 10.002$
For spot prices less than a dollar, the precision factor decreases
(increasing the incremental precision) at every factor of 10:
- $tick_{-100} = 0.99999$
- $tick_{-200} = 0.99998$
- $tick_{-500100} = 0.94999$
- $tick_{-500200} = 0.94998$
- $tick_{-9000100} = 0.099999$
- $tick_{-9000200} = 0.099998$
This goes on in the negative direction until it reaches a spot price of
0.000000000000000001 or in the positive direction until it reaches a spot
price of 100000000000000000000000000000000000000.
The minimum spot price was chosen as this is the smallest possible number
supported by the osmomath.Dec type. As for the maximum spot price, the above number
was based on gamm's max spot price of 340282366920938463463374607431768211455.
While these numbers are not the same, the max spot price used in concentrated
liquidity utilizes the same number of significant figures as gamm's max spot
price and is less than gamm's max spot price which satisfies the initial design requirements.
### Formulas
After we define tick spacing (which effectively defines the $exponentAtPriceOne$,
since $exponentAtPriceOne$ is fixed), we can then calculate how many ticks must
be crossed in order for $k$ to be incremented
( $geometricExponentIncrementDistanceInTicks$ ).
$$geometricExponentIncrementDistanceInTicks = 9 * 10^{(-exponentAtPriceOne)}$$
Since we define $exponentAtPriceOne$ and utilize this as the increment starting
point instead of price zero, we must multiply the result by 9 as shown above.
In other words, starting at 1, it takes 9 ticks to get to the first power of 10.
Then, starting at 10, it takes 9\*10 ticks to get to the next power of 10, etc.
Now that we know how many ticks must be crossed in order for our
$exponentAtPriceOne$ to be incremented, we can then figure out what our change
in $exponentAtPriceOne$ will be based on what tick is being traded at:
$$geometricExponentDelta = ⌊ tick / geometricExponentIncrementDistanceInTicks ⌋$$
With $geometricExponentDelta$ and $exponentAtPriceOne$, we can figure out what
the $exponentAtPriceOne$ value we will be at when we reach the provided tick:
$$exponentAtCurrentTick = exponentAtPriceOne + geometricExponentDelta$$
Knowing what our $exponentAtCurrentTick$ is, we must then figure out what power
of 10 this $exponentAtPriceOne$ corresponds to (by what number does the price
gets incremented with each new tick):
$$currentAdditiveIncrementInTicks = 10^{(exponentAtCurrentTick)}$$
Lastly, we must determine how many ticks above the current increment we are at:
$$numAdditiveTicks = tick - (geometricExponentDelta * geometricExponentIncrementDistanceInTicks)$$
With this, we can determine the price:
$$price = (10^{geometricExponentDelta}) + (numAdditiveTicks * currentAdditiveIncrementInTicks)$$
where $(10^{geometricExponentDelta})$ is the price after $geometricExponentDelta$
increments of $exponentAtPriceOne$ (which is basically the number of decrements
of difference in price between two adjacent ticks by the power of 10)
### Tick Spacing Example: Tick to Price
Bob sets a limit order on the USD<>BTC pool at tick 36650010. This pool's
$exponentAtPriceOne$ is -6. What price did Bob set his limit order at?
$$geometricExponentIncrementDistanceInTicks = 9 * 10^{(6)} = 9000000$$
$$geometricExponentDelta = ⌊ 36650010 / 9000000 ⌋ = 4$$
$$exponentAtCurrentTick = -6 + 4 = -2$$
$$currentAdditiveIncrementInTicks = 10^{(-2)} = 0.01$$
$$numAdditiveTicks = 36650010 - (4 * 9000000) = 650010$$
$$price = (10^{4}) + (650010 * 0.01) = 16,500.10$$
Bob set his limit order at price $16,500.10
### Tick Spacing Example: Price to Tick
Bob sets a limit order on the USD<>BTC pool at price $16,500.10. This pool's
$exponentAtPriceOne$ is -6. What tick did Bob set his limit order at?
$$geometricExponentIncrementDistanceInTicks = 9 * 10^{(6)} = 9000000$$
We must loop through increasing exponents until we find the first exponent that
is greater than or equal to the desired price
$$currentPrice = 1$$
$$ticksPassed = 0$$
$$currentAdditiveIncrementInTicks = 10^{(-6)} = 0.000001$$
$$maxPriceForCurrentAdditiveIncrementInTicks = geometricExponentIncrementDistanceInTicks
* currentAdditiveIncrementInTicks = 9000000 * 0.000001 = 9$$
$$ticksPassed = ticksPassed + geometricExponentIncrementDistanceInTicks =
0 + 9000000 = 9000000$$
$$totalPrice = totalPrice + maxPriceForCurrentAdditiveIncrementInTicks =
1 + 9 = 10$$
10 is less than 16,500.10, so we must increase our exponent and try again
$$currentAdditiveIncrementInTicks = 10^{(-5)} = 0.00001$$
$$maxPriceForCurrentAdditiveIncrementInTicks = geometricExponentIncrementDistanceInTicks
* currentAdditiveIncrementInTicks = 9000000 * 0.00001 = 90$$
$$ticksPassed = ticksPassed + geometricExponentIncrementDistanceInTicks =
9000000 + 9000000 = 18000000$$
$$totalPrice = totalPrice + maxPriceForCurrentAdditiveIncrementInTicks =
10 + 90 = 100$$
100 is less than 16,500.10, so we must increase our exponent and try again.
This goes on until...
$$currentAdditiveIncrementInTicks = 10^{(-2)} = 0.01$$
$$maxPriceForCurrentAdditiveIncrementInTicks = geometricExponentIncrementDistanceInTicks
* currentAdditiveIncrementInTicks = 9000000 * 0.01 = 90000$$
$$ticksPassed = ticksPassed + geometricExponentIncrementDistanceInTicks =
36000000 + 9000000 = 45000000$$
$$totalPrice = totalPrice + maxPriceForCurrentAdditiveIncrementInTicks =
10000 + 90000 = 100000$$
100000 is greater than 16,500.10. This means we must now find out how many
additive tick in the currentAdditiveIncrementInTicks of -2 we must pass in
order to reach 16,500.10.
$$ticksToBeFulfilledByExponentAtCurrentTick = (desiredPrice - totalPrice) /
currentAdditiveIncrementInTicks = (16500.10 - 100000) / 0.01 = -8349990$$
$$tickIndex = ticksPassed + ticksToBeFulfilledByExponentAtCurrentTick =
45000000 + -8349990 = 36650010$$
Bob set his limit order at tick 36650010
## Chosing an Exponent At Price One Value
The creator of a pool cannot choose an exponenetAtPriceOne as one of the input
parameters since it is hard coded to -6. The number can be psedo-controlled by
choosing the tick spacing a pool is initialized with. For example, if a pool
is desired to have an exponentAtPriceOne of -6, the pool creator can choose a
tick spacing of 1. If a pool is desired to have an exponentAtPriceOne of -4,
this is two factors of 10 greater than -6, so the pool creator can choose a
tick spacing of 100 to achieve this level of precision.
As explained previously, the exponent at price one determines how much the spot
price increases or decreases when traversing ticks. The following equation will
assist in selecting this value:
$$exponentAtPriceOne=log_{10}(\frac{D}{P})$$
$$P=(\frac{baseAssetInUSD}{quoteAssetInUSD})$$
$$D=P-(\frac{baseAssetInUSD}{quoteAssetInUSD+desiredIncrementOfQuoteInUSD})$$
### Example 1
SHIB is trading at $0.00001070 per SHIB
BTC is trading at $28,000 per BTC
We want to create a SHIB/BTC concentrated liquidity pool where SHIB is the
baseAsset (asset0) and BTC is the quoteAsset (asset1). In terms of the quoteAsset,
we want to increment in 10 cent values.
$$P=(\frac{0.00001070}{28,000})=0.000000000382142857$$
$$D=(0.000000000382142857)-(\frac{0.00001070}{28,000+0.10})=0.0000000000000013647910441136$$
$$exponentAtPriceOne=log_{10}(\frac{0.0000000000000013647910441136}{0.000000000382142857})=-5.447159582$$
We can therefore conclude that we can use an exponent at price one of -5
(slightly under precise) or -6 (slightly over precise) for this base/quote pair
and desired price granularity. This means we would either want a tick spacing of 1
(to have an exponent at price one of -6) or 10 (to have an exponent at price one of -5).
### Example 2
Flipping the quoteAsset/baseAsset, for BTC/SHIB, lets determine what the
exponentAtPriceOne should be. For SHIB as a quote, centralized exchanges
list prices at the 10^-8, so we will set our desired increment to this value.
$$P=(\frac{28,000}{0.00001070})=2616822429$$
$$D=(2616822429)-(\frac{28,000}{0.00001070+0.00000001})=2443345$$
$$exponentAtPriceOne=-log_{10}(\frac{2443345}{2616822429})=-3.0297894598783$$
We can therefore conclude that we can use an exponent at price one of -3
for this base/quote pair and desired price granularity. This means we would
want a tick spacing of 1000 (to have an exponent at price one of -3).
### Consequences
This decision allows us to define ticks at spot prices that users actually
desire to trade on, rather than arbitrarily defining ticks at .01% distance
between each other. This will also make integration with UX seamless,
instead of either:
a) Preventing trade at a desirable spot price or
b) Having the front end round the tick's actual price to the nearest
human readable/desirable spot price
One side effect of increasing precision as we get closer to the minimum tick
is that multiple ticks can represent the same price. For example, tick
-161795100 (along with the ticks surrounding it) correlate to a price
of 0.000000000000000002. To get around any issues this may cause, when a
position is created with a user defined lower and upper tick, we determine
if a larger tick exists that represents the same price. If so, we use that tick
instead of the user defined tick. In the above example, the tick would be
changed to -161000000, which is the first tick that represents the same price.
## Concentrated Liquidity Module Messages
### `MsgCreatePosition`
- **Request**
This message allows LPs to provide liquidity between `LowerTick` and `UpperTick`
in a given `PoolId`. The user provides the amount of each token desired. 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.
Three KV stores are initialized when a position is created:
1. `Position ID -> Position` - This is a mapping from a unique position ID to a
position object. The position ID is a monotonically increasing integer that is
incremented every time a new position is created.
2. `Owner | Pool ID | Position ID -> Position ID` - This is a mapping from a
composite key of the owner address, pool ID, and position ID to the position ID.
This is used to keep track of all positions owned by a given owner in a given pool.
3. `Pool ID -> Position ID` - This is a mapping from a pool ID to a position ID.
This is used to keep track of all positions in a given pool.
```go
type MsgCreatePosition struct {
PoolId uint64
Sender string
LowerTick int64
UpperTick int64
TokenDesired0 types.Coin
TokenDesired1 types.Coin
TokenMinAmount0 github_com_cosmos_cosmos_sdk_types.Int
TokenMinAmount1 github_com_cosmos_cosmos_sdk_types.Int
}
```
- **Response**
On succesful response, we receive the actual amounts of each token used to
create the liquidityCreated number of shares in the given range.
```go
type MsgCreatePositionResponse struct {
PositionId uint64
Amount0 github_com_cosmos_cosmos_sdk_types.Int
Amount1 github_com_cosmos_cosmos_sdk_types.Int
JoinTime google.protobuf.Timestamp
LiquidityCreated github_com_cosmos_cosmos_sdk_types.Dec
}
```
This message should call the `createPosition` keeper method that is introduced
in the `"Liquidity Provision"` section of this document.
### `MsgWithdrawPosition`
- **Request**
This message allows LPs to withdraw their position via their position ID,
potentially in partial amount of liquidity. It should fail if the position ID
does not exist or if attempting to withdraw an amount higher than originally
provided. If an LP withdraws all of their liquidity from a position, then the
position is deleted from state along with the three KV stores that were
initialized in the `MsgCreatePosition` section. However, the spread factor accumulators
associated with the position are still retained until a user claims them manually.
```go
type MsgWithdrawPosition struct {
PositionId uint64
Sender string
LiquidityAmount github_com_cosmos_cosmos_sdk_types.Dec
}
```
- **Response**
On successful response, we receive the amounts of each token withdrawn
for the provided share liquidity amount.
```go
type MsgWithdrawPositionResponse struct {
Amount0 github_com_cosmos_cosmos_sdk_types.Int
Amount1 github_com_cosmos_cosmos_sdk_types.Int
}
```
This message should call the `withdrawPosition` keeper method that is introduced
in the `"Liquidity Provision"` section of this document.
### `MsgCreatePool`
This message is responsible for creating a concentrated-liquidity pool.
It propagates the execution flow to the `x/poolmanager` module for pool id
management and for routing swaps.
```go
type MsgCreateConcentratedPool struct {
Sender string
Denom0 string
Denom1 string
TickSpacing uint64
SpreadFactor github_com_cosmos_cosmos_sdk_types.Dec
}
```
- **Response**
On successful response, the pool id is returned.
```go
type MsgCreateConcentratedPoolResponse struct {
PoolID uint64
}
```
### `MsgCollectSpreadRewards`
This message allows collecting rewards from spreads for multiple position IDs from a
single owner.
The spread factor collection is discussed in more detail in the "Spread Rewards" section of this document.
```go
type MsgCollectSpreadRewards struct {
PositionIds []uint64
Sender string
}
```
- **Response**
On successful response, the collected tokens are returned.
The sender should also see their balance increase by the returned
amounts.
```go
type MsgCollectSpreadRewardsResponse struct {
CollectedSpreadRewards []types.Coin
}
```
### `MsgFungifyChargedPositions`
This message allows fungifying the fully charged unlocked positions belonging to the same owner
and located in the same tick range.
MsgFungifyChargedPosition takes in a list of positionIds and combines them into a single position.
It validates that all positions belong to the same owner, are in the same ticks and are fully charged.
Fails if not. Otherwise, it creates a completely new position P. P's liquidity equals to the sum of all
liquidities of positions given by positionIds. The uptime of the join time of the new position equals
to current block time - max authorized uptime duration (to signify that it is fully charged).
The previous positions are deleted from state. Prior to deleting, the rewards are claimed.
The old position's unclaimed rewards are transferred to the new position.
The new position ID is returned.
```go
type MsgFungifyChargedPositions struct {
PositionIds []uint64
Sender string
}
```
- **Response**
On successful response, the new position id is returned.
```go
type MsgFungifyChargedPositionsResponse struct {
NewPositionId uint64
}
```
## Relationship to Pool Manager Module
### Pool Creation
As previously mentioned, the `x/poolmanager` is responsible for creating the
pool upon being called from the `x/concentrated-liquidity` module's message server.
It does so to store the mapping from pool id to concentrated-liquidity module so
that it knows where to route swaps.
Upon successful pool creation and pool id assignment, the `x/poolmanager` module
returns the execution to `x/concentrated-liquidity` module by calling `InitializePool`
on the `x/concentrated-liquidity` keeper.
The `InitializePool` method is responsible for doing concentrated-liquidity specific
initialization and storing the pool in state.
Note, that `InitializePool` is a method defined on the `SwapI` interface that is
implemented by all swap modules. For example, `x/gamm` also implements it so that
`x/pool-manager` can route pool initialization there as well.
### Swaps
We rely on the swap messages located in `x/poolmanager`:
- `MsgSwapExactAmountIn`
- `MsgSwapExactAmountOut`
The `x/poolmanager` received the swap messages and, as long as the swap's pool id
is associated with the `concentrated-liquidity` pool, the swap is routed
into the relevant module. The routing is done via the mapping from state that was
discussed in the "Pool Creation" section.
## Liquidity Provision
> As an LP, I want to provide liquidity in ranges so that I can achieve greater
capital efficiency
This is a basic function that should allow LPs to provide liquidity in specific ranges
to a pool.
A pool's liquidity is consisted of two assets: asset0 and asset1. In all pools,
asset1 will be the quote asset and must be an approved denom listed in the module
parameters. At the current tick, the bucket at this tick consists of a mix of both
asset0 and asset1 and is called the virtual liquidity of the pool (or "L" for short).
Any positions set below the current price are consisted solely of asset0 while
positions above the current price only contain asset1.
### Adding Liquidity
We can either provide liquidity above or below the current price, which would
act as a range order, or decide to provide liquidity at the current price.
As declared in the API for `createPosition`, users provide the upper and lower
tick to denote the range they want to provide the liquidity in. The users are
also prompted to provide the amount of token0 and token1 they desire to receive.
The liquidity that needs to be provided for the given token0 and token1 amounts
would be then calculated by the following methods:
Liquidity needed for token0:
$$L = \frac{\Delta x \sqrt{P_u} \sqrt{P_l}}{\sqrt{P_u} - \sqrt{P_l}}$$
Liquidity needed for token1:
$$L = \frac{\Delta y}{\sqrt{P_u}-\sqrt{P_l}}$$
Then, we pick the smallest of the two values for choosing the final `L`. The
reason we do that is because the new liquidity must be proportional to the old
one. By choosing the smaller value, we distribute the liqudity evenly between
the two tokens. In the future steps, we will re-calculate the amount of token0
and token1 as a result the one that had higher liquidity will end up smaller
than originally given by the user.
Note that the liquidity used here does not represent an amount of a specific
token, but the liquidity of the pool itself, represented in `osmomath.Dec`.
Using the provided liquidity, now we calculate the delta amount of both token0
and token1, using the following equations, where L is the liquidity calculated above:
$$\Delta x = \frac{L(\sqrt{p(i_u)} - \sqrt{p(i_c)})}{\sqrt{p(i_u)}\sqrt{p(i_c)}}$$
$$\Delta y = L(\sqrt{p(i_c)} - \sqrt{p(i_l)})$$
Again, by recalculating the delta amount of both tokens, we make sure that the
new liquidity is proportional to the old one and the excess amount of the token
that originally computed a larger liquidity is given back to the user.
The delta X and the delta Y are the actual amounts of tokens joined for the
requested position.
Given the parameters needed for calculating the tokens needed for creating a
position for a given tick, the API in the keeper layer would look like the following:
```go
ctx sdk.Context, poolId uint64, owner sdk.AccAddress, amount0Desired,
amount1Desired, amount0Min, amount1Min osmomath.Int,
lowerTick, upperTick int64, frozenUntil time.Time
func createPosition(
ctx sdk.Context,
poolId uint64,
owner sdk.AccAddress,
amount0Desired,
amount1Desired,
amount0Min,
amount1Min osmomath.Int
lowerTick,
upperTick int64) (amount0, amount1 osmomath.Int, osmomath.Dec, error) {
...
}
```
### Removing Liquidity
Removing liquidity is achieved via method `withdrawPosition` which is the inverse
of previously discussed `createPosition`. In fact, the two methods share the same
underlying logic, having the only difference being the sign of the liquidity.
Plus signifying addition while minus signifying subtraction.
Withdraw position also takes an additional parameter which represents the liqudity
a user wants to remove. It must be less than or equal to the available liquidity
in the position to be successful.
```go
func (k Keeper) withdrawPosition(
ctx sdk.Context,
poolId uint64,
owner sdk.AccAddress,
lowerTick,
upperTick int64,
frozenUntil time.Time,
requestedLiquidityAmountToWithdraw osmomath.Dec)
(amtDenom0, amtDenom1 osmomath.Int, err error) {
...
}
```
## Swapping
> As a trader, I want to be able to swap over a concentrated liquidity pool so
that my trades incur lower slippage
Unlike balancer pools where liquidity is spread out over an infinite range,
concentrated liquidity pools allow for LPs to provide deeper liquidity for
specific price ranges, which in turn allows traders to incur less slippage on
their trades.
Despite this improvement, the liquidity at the current price is still finite,
and large single trades in times of high volume, as well as trades against
volatile assets, are eventually bound to incur some slippage.
In order to determine the depth of liquidity and subsequent amountIn/amountOut
values for a given pool, we track the swap's state across multiple swap "steps".
You can think of each of these steps as the current price following the original
xy=k curve, with the far left bound being the next initialized tick below the
current price and the far right bound being the next initialized tick above the
current price. It is also important to note that we always view prices of asset1
in terms of asset0, and selling asset1 for asset0 would, in turn, increase its
spot price. The reciprocal is also true, where if we sell asset0 for asset1,
we would decrease the pool's spot price.
When a user swaps asset0 for asset1 (can also be seen as "selling" asset0), we
move left along the curve until asset1 reserves in this tick are depleted.
If the tick of the current price has enough liquidity to fulfill the order without
stepping to the next tick, the order is complete. If we deplete all of asset1 in
the current tick, this then marks the end of the first swap "step". Since all
liquidity in this tick has been depleted, we search for the next closest tick
to the left of the current tick that has liquidity. Once we reach this tick, we
determine how much more of asset1 is needed to complete the swap. This process
continues until either the entire order is fulfilled or all liquidity is drained
from the pool.
The same logic is true for swapping asset1, which is analogous to buying asset0;
however, instead of moving left along the set of curves, we instead search for
liquidity to the right.
From the user perspective, there are two ways to swap:
1. Swap given token in for token out.
- E.g. I have 1 ETH that I swap for some computed amount of DAI.
2. Swap given token out for token in
- E.g. I want to get out 3000 DAI for some amount of ETH to compute.
Each case has a corresponding message discussed previosly in the `x/poolmanager`
section.
- `MsgSwapExactIn`
- `MsgSwapExactOut`
Once a message is received by the `x/poolmanager`, it is propageted into a
corresponding keeper
in `x/concentrated-liquidity`.
The relevant keeper method then calls its non-mutative `calc` version which is
one of:
- `calcOutAmtGivenIn`
- `calcInAmtGivenOut`
State updates only occur upon successful execution of the swap inside the calc method.
We ensure that calc does not update state by injecting `sdk.CacheContext` as its
context parameter. The cache context is dropped on failure and committed on success.
### Calculating Swap Amounts
Let's now focus on the core logic of calculating swap amounts.
We mainly focus on `calcOutAmtGivenIn` as the high-level steps of `calcInAmtGivenOut`
are similar.
**1. Determine Swap Strategy**
The first step we need to determine is the swap strategy. The swap strategy determines
the direction of the swap, and it is one of:
- `zeroForOne` - swap token zero in for token one out.
- `oneForZero` - swap token one in for token zero out.
Note that the first token in the strategy name always corresponds to the token
being swapped in, while the second token corresponds to the token being swapped
out. This is true for both `calcOutAmtGivenIn` and `calcInAmtGivenOut` calc methods.
Recall that, in our model, we fix the tokens axis at the time of pool creation.
The token on the x-axis is token zero, while the token on the y-axis is token one.
Given that the sqrt price is defined as $$\sqrt (y / x)$$, as we swap token zero
(x-axis) in for token one (y-axis), we decrease the sqrt price and move down
along the price/tick curve. Conversely, as we swap token one (y-axis) in for token
zero (x-axis), we increase the sqrt price and move up along the price/tick curve.
The reason we call this a price/tick curve is because there is a relationship
between the price and the tick. As a result, when we perform the swap, we are
likely to end up crossing a tick boundary. As a tick is crossed, the swap state
internals must be updated. We will discuss this in more detail later.
**2. Initialize Swap State**
The next step is to initialize the swap state. The swap state is a struct that
contains all of the swap state to be done within the current active tick
(before we across a tick boundary).
It contains the following fields:
```go
// SwapState defines the state of a swap.
// It is initialized as the swap begins and is updated after every swap step.
// Once the swap is complete, this state is either returned to the estimate
// swap querier or committed to state.
type SwapState struct {
// Remaining amount of specified token.
// if out given in, amount of token being swapped in.
// if in given out, amount of token being swapped out.
// Initialized to the amount of the token specified by the user.
// Updated after every swap step.
amountSpecifiedRemaining osmomath.Dec
// Amount of the other token that is calculated from the specified token.
// if out given in, amount of token swapped out.
// if in given out, amount of token swapped in.
// Initialized to zero.
// Updated after every swap step.
amountCalculated osmomath.Dec
// Current sqrt price while calculating swap.
// Initialized to the pool's current sqrt price.
// Updated after every swap step.
sqrtPrice osmomath.Dec
// Current tick while calculating swap.
// Initialized to the pool's current tick.
// Updated each time a tick is crossed.
tick osmomath.Int
// Current liqudiity within the active tick.
// Initialized to the pool's current tick's liquidity.
// Updated each time a tick is crossed.
liquidity osmomath.Dec
// Global spread reward growth per-current swap.
// Initialized to zero.
// Updated after every swap step.
spreadRewardGrowthGlobal osmomath.Dec
}
```
**3. Compute Swap**
The next step is to compute the swap. Conceptually, it can be done in two ways
listed below.Before doing so, we find the next initialized tick. An initialized
tick is the tick that is touched by the edges of at least one position. If no
position has an edge at a tick, then that tick is uninitialized.
a. Swap within the same initialized tick range.
See "Appendix A" for details on what "initialized" means.
This case occurs when `swapState.amountSpecifiedRemaining` is less than or equal
to the amount needed to reach the next tick. We omit the math needed to determine
how much is enough until a later section.
b. Swap across multiple initialized tick ranges.
See "Appendix A" for details on what "initialized" means.
This case occurs when `swapState.amountSpecifiedRemaining` is greater than the
amount needed to reach the next tick
In terms of the code implementation, we loop, calling a `swapStrategy.ComputeSwapStepOutGivenIn`
or `swapStrategy.ComputeSwapStepInGivenOut` method, depending on swap out given
in or in given out, respectively.
The swap strategy is already initialized to be either `zeroForOne` or `oneForZero`
from step 1. Go dynamically determines the desired implementation via polymorphism.
We leave details of the `ComputeSwapStepOutGivenIn` and `ComputeSwapStepInGivenOut`
methods to the appendix of the "Swapping" section.
The iteration stops when `swapState.amountSpecifiedRemaining` runs out or when
swapState.sqrtPrice reaches the sqrt price limit specified by the user as a price
impact protection.
**4. Update Swap State**
Upon computing the swap step, we update the swap state with the results of the
swap step. Namely,
- Subtract the consumed specified amount from `swapState.amountSpecifiedRemaining`.
- Add the calculated amount to `swapState.amountCalculated`.
- Update `swapState.sqrtPrice` to the new sqrt price. The new sqrt price is not
necessarily the sqrt price of the next tick. It is the sqrt price of the next tick
if the swap step crosses a tick boundary. Otherwise, it is something in between
the original and the next tick sqrt price.
- Update `swapState.tick` to the next initialized tick if it is reached;
otherwise, update it to the new tick calculated from the new sqrt price.
If the sqrt price is unchanged, the tick remains unchanged as well.
- Update `swapState.liquidity` to the new liquidity only if the next initialized
tick is crossed. The liquidity is updated by incorporating the `liquidity_net`
amount associated with the next initialized tick being crossed.
- Update `swapState.spreadRewardGrowthGlobal` to the value of the total spread factor charged within
the swap step on the amount of token in per one unit of liquidity within the
tick range being swapped in.
Then, we either proceed to the next swap step or finalize the swap.
**5. Update Global State**
Once the swap is completed, we persiste the swap state to the global state
(if mutative action is performed) and return the `amountCalculated` to the user.
## Liquidity depths calculation
### Calculating liquidity for buckets
Each bucket (the area between two initialized ticks) contains ceratin amount of liquidity. The liquidity amount can be obtained through `GetTickLiquidityNetInDirection` query. `GetTickLiquidityNetInDirection` returns two results:
- `expectedStartTickLiquidity` which is the global liquidity, the cumulative liquidity from the bucket of the current price
- `expectedLiquidityDepths` which is list of liquidity deltas for each and every initialized tick for the full price range in a certain direction that should be cumulatively added to or subtracted starting from the global liquidity in order to get the liquidity for an arbitrary range.
Querying in two different directions is possible, one being 'one for zero', which is in the direction of max tick, the other direction being 'zero for one', which is in the direction of min tick.
If the direction is zero for one, the liquidity for a bucket in tick can be calculated as follows: L<sub>t</sub> = L<sub>t-1</sub> - ΔL<sub>t</sub>, t=1,2, ...
If the direction is one for zero, the liquidity for the bucket in the designated tick can be calculated as follows: L<sub>t</sub> = L<sub>t-1</sub> + ΔL<sub>t</sub>, t=1,2
where L<sub>0</sub> is the global(cumulative liquidity).
### Deducing the quantity of tokens X and Y for a tick range
Having obtained the liquidity depths for each liquidity buckets in the pool, we can derive an equation to calculate the quantity of each token locked for a certain price range. Let *i* and *j* be the indexes of lower and upper tick boundaries of the range we want to calculate, let P<sub>0</sub> be current price and P<sub>a</sub>, P<sub>b</sub> prices for lower tick and upper tick respectively, where P<sub>a</sub>, P<sub>b</sub> are defined as the following:
<p align="center">
P<sub>a</sub> = 1.0001<sup>i</sup>, P<sub>b</sub> = 1.0001<sup>j</sup>
</p>
Let *L* be the total liquidity locked within price range [P<sub>a</sub>, P<sub>b</sub>]. The real reserve curve with liquidity *L* is:
$(x + \frac{L}{\sqrt{P_b}})(y + L\sqrt{P_a}) = L^2$
Given the amount of liquidity, the quantity of tokens available in the price range [P<sub>a</sub>, P<sub>b</sub>] can be derived as follows:
- If $P_0 \leq P_a$:
$x = L \left(\frac{1}{\sqrt{P_a}} - \frac{1}{\sqrt{P_b}}\right)$
$y = 0$
- If $P_0 \geq P_b$:
$x = 0$
$y = L \left(\sqrt{P_b} - \sqrt{P_a}\right)$
- If $P_0 \in (P_a, P_b)$:
$x = L \left(\frac{1}{{\sqrt{P_0}}} - \frac{1}{{\sqrt{P_b}}}\right)$
$y = L \left(\sqrt{P_0} - \sqrt{P_a}\right)$
This can be written as:
$x = L \left(\frac{1}{\sqrt{z}} - \frac{1}{\sqrt{P_b}}\right)$
$y = L \left(\sqrt{z} - \sqrt{P_a}\right)$
where:
$z = P_a \quad \forall \quad P \leq P_a$
$z = P_0 \quad \forall \quad P \in (P_a, P_b)$
$z = P_b \quad \forall \quad P \geq P_b$
### Calculating Pool Depths
Pool depths here refers to the token required to shift the pool price to a certain tick.
Let i<sub>0</sub> be the integer index identifying a current tick (this is the tick that corresponds to the square root of the current price). Let δ be the desired depth level and d be the associated change in ticks such that
$\delta = \frac{P_1}{P_0} - 1 = \frac{1.0001^{(i_0 + d)}}{1.0001^{i_0}} - 1$
The change in ticks $d$ can be found as follows:
$d = \log_{1.0001}(\delta + 1)$
e.g. $d = -513$ for $\delta = -5\%$
Note that as the current price formally belongs only to the first range [s<sub>0</sub>, s<sub>1</sub>] or [s<sub>1</sub>, s<sub>0</sub>], where s<sub>0</sub> is the left (when the price grows) or the right (when the price drops) boundary of the current liquidity segment (segment that contains the current price). Mathematically, as we always start calculations from the current price, when recovering the token amounts, all ranges contain only one of the tokens (X or Y depending on the direction of the price change). When swapping, one of the tokens is exchanged for the token being sold. We need to find how much we can swap until the price reaches a certain level.
The Pool Depths expressed in units of asset X or Y in calculated as follows:
$Depth_{x}(\delta) = \sum_{i=0}^{n}x_{i}$
$Depth_{y}(\delta) = \sum_{i=0}^{n}y_{i}$
where:
$x_i = L_i \left(\frac{1}{\sqrt{P_{a}(i)}} - \frac{1}{\sqrt{P_{b}(i)}}\right), i = 0, \dots, n$
$y_i = L_i \left(\sqrt{P_{b}(i)} - \sqrt{P_{a}(i)}\right), i = 0, \dots, n$
### Calculating Liquidator Depths
Liquidator Depth refers to the real price slippage, the real price slippage (average execution price in relation to the current price) from the liquidator's point of view. That is, the amount of tokens that can be liquidated so that the real price slippage does not exceed certain level.
The idea to determine the depth is to swap the maximum possible amount per each liquidity segment until a realized slippage doesn’t exceed a desired level. For the last segment the procedure is repeated per tick until the desired realized slippage is achieved.
Maximum token amounts that can be locked within each segment or tick (and exchanged for another token when the price crosses the ticks) can be derived as follows:
$x_i = L_i \left(\frac{1}{\sqrt{P_{a}(i)}} - \frac{1}{\sqrt{P_{b}(i)}}\right)$
$y_i = L_i \left(\sqrt{P_{b}(i)} - \sqrt{P_{a}(i)}\right)$
Starting from the current tick *i<sub>0</sub>* and swapping one token for another one, the average execution price at the end of each tick *i* can be determined as follows:
$P_i = \frac{(Y_{i-1} + y_i)}{X_{i-1} + x}, \quad i = i_0, \ldots, i_N$
$Y_i = Y_{i-1} + y_i, \quad Y_0 = 0$
$X_i = X_{i-1} + x_i, \quad X_0 = 0$
The slippage equals to:
$Slippage_i = P_i - P_0 - 1$
The algorithm to determine the depth is then the following:
Calculate $X_i$ and $Y_i$ while $|Slippage_i| \leq \delta$ at some $i = N$
Then
$Depth_x (\delta) = X_N$
$Depth_y (\delta) = Y_N$
## Migration
Users can migrate their Balancer positions to a Concentrated Liquidity full range
position provided the underlying Balancer pool has a governance-selected canonical
Concentrated Liquidity pool. The migration is routed depending on the state of the
underlying Balancer position:
Balancer position is:
- Superfluid delegated
- Locked
- Superfluid undelegating
- Locked
- Unlocking
- Normal lock
- Locked
- Unlocking