Pool Process Flows
Automating Tried And Tested Procedures
Last updated
Automating Tried And Tested Procedures
Last updated
No action (minting / burning / swaps) can be performed prior to pool initialization.
In addition to setting the initial sqrt price, a small amount of token0 and token1 is required to be seeded for the initialization of reinvestL
to the value of MIN_LIQUIDITY
. This is required to prevent division by zero in the calcRMintQty()
function when swaps are performed. MIN_LIQUIDITY
was chosen to be reasonably small enough to avoid calculation inaccuracies for swaps, and from taking unreasonably large capital amounts from the caller.
As part of this anti-spam feature, Elastic allocates token weis as liquidity for the reinvestment curve. While this amount has been carefully selected to suit the majority of tokens, there are rare exceptions where token teams decide to implement tokens with less decimals for reasons of their own. For reference, the majority of ERC20 tokens (LINK, MATIC, ANKR, SHIB, etc.) are created with decimals, WBTC has decimals, major stablecoin tokens like USDC/USDT have decimals.
In cases where the token has a low decimal value and the per unit value of the token high, the amount taken might be of significant value (i.e. a token is created with decimals and each unit has a 1USD value, which results in tokens with a value of 100USD being taken as an anti-spam feature). As a permissionless platform, KyberSwap supports the listing of all tokens which meet the ERC20 standard and as such, users are responsible for checking if their token falls into the aforementioned category.
Adding and removing liquidity have very similar flows. One of the main differences is that mint()
is possibly a permissioned function, but burn()
is not. More information relating to the requirement for this can be found in this section on whitelisting position managers.
A simple check is performed to ensure that the requested liquidity amount to mint / burn is non-zero
_tweakPosition()
is called, which does the following:
Load the pool state into memory poolData
(current price, tick and liquidity values)
Call _syncFeeGrowth()
to update fee growth data. Mints reinvestment tokens if necessary
Call _syncSecondsPerLiquidity()
to update seconds per liquidity data
The updated global values and poolData
is passed into _updatePosition()
Updates (initializes) the lower and upper position ticks. Will insert or remove the tick from the linked list whenever necessary
Calculates feeGrowthInside and returns the amount of reinvestment tokens claimable by the position
Transfers the claimable reinvestment tokens to the position owner, if any
Calculates the token0 and token1 quantity required to be collected from (add liquidity) or sent to (remove liquidity) msg.sender
. Will apply liquidity changes to pool liquidity if the specified position is active
In the case of adding liquidity, a callback is made to collect the tokens
Emit event
Like KyberSwap Classic
, there are 4 different types of swaps available that a user can specify.
Swap from a specified amount of token 0 (exactInput0)
Swap from a specified amount of token 1 (exactInput1)
Swap to a specified amount of token 0 (exactOutput0)
Swap to a specified amount of token 1 (exactOutput1)
Swapping token 0 for token 1 (cases 1 and 4) cause the pool price and tick to move downwards, while swapping token 1 for token 0 (cases 2 and 3) cause the pool price and tick to move upwards.
In addition, the user can specify a price limit that the swap can reach. The minimum and maximum price limits a user can specify is MIN_SQRT_RATIO + 1
and MAX_SQRT_RATIO - 1
.
The algorithm exits when either the specified amount has been fully used, or if the price limit has been reached.
The swap amount is a int256
to implicitly suggest whether it is exact input (> 0) or exact output (< 0).
Fetch the initial pool state
Call SwapMath.computeSwapStep()
to calculate the actual swap input and output amounts to be used, swap fee amount and next pool price
Subtract amount to be used (usedAmount
) to swapData.specifiedAmount
Add amount to be sent to user (returnedAmount
) to swapData.returnedAmount
Check if swap will reach next tick
If true, set swapData.currentTick = willUpTick ? tempNextTick : tempNextTick - 1
and continue
If false, recalculate the current tick based on current price and break the loop
Load variables (if not loaded already) that are initialized when crossing ticks
Calculate amount of reinvestment tokens to be minted for fees to be sent to government and to for LP contributions, and update feeGrowthGlobal
Perform actual minting of reinvestment tokens if necessary
Update pool state (price, ticks, liquidity, feeGrowth, reinvestment variables)
Send token to caller, execute swap callback to collect token
Negative quantity = transfer to caller
Positive quantity = collect from caller
computeSwapStep()
FlowInputs
liquidity
uint256
active base liquidity + reinvestment liquidity
currentSqrtP
uint160
current sqrt price
targetSqrtP
uint160
sqrt price limit nextSqrtP
can take
feeInBps
uint256
swap fee in basis points
specifiedAmount
int256
amount remaining to be used for the swap
isExactInput
bool
true if specifiedAmount
refers to input amount, false if specifiedAmount
refers to output amount
isToken0
bool
true if specifiedAmount
is in token0, false if specifiedAmount
is in token1
Outputs
usedAmount
int256
actual amount to be used for the swap. >= 0 if isExactInput
= true, <= 0 if isExactInput
= false
returnedAmount
int256
output qty (<= 0) to be accumulated if isExactInput
= true, input qty (>= 0) if isExactInput
= false
deltaL
uint256
collected swap fee, to be incremented to reinvest liquidity
nextSqrtP
uint160
new sqrt price after the computed swap step
Calculate the amount required to reach targetSqrtP
from currentSqrtP
by calling calcReachAmount()
.
If amount required exceeds specifiedAmount
, then the targetPrice will not be reached, and we expect the resulting price nextSqrtP
to not exceed targetSqrtP
.
usedAmount := specifiedAmount
Calculate the final price nextSqrtP
by calling calcFinalPrice()
Otherwise, the temporary next tick will be crossed.
usedAmount
will be the amount calculated in step 1
set the resulting price nextSqrtP
= targetSqrtP
Finally, calculate returnedAmount
by calling calcReturnedAmount()
.
Assume that:
x1, x2: the amount of token0 before/after swap
y1, y2: the amount of token1 before/after swap
L1, L2: the liquidity before/after swap
p1, p2: the price before/after swap
This can be transformed into
:= pool.baseL (liquidity provided by positions)
:= pool.reinvestL (liquidity from fees collected)
:= pool.sqrtP (current sqrt price of token1/token0)
:= pool.currentTick (tick associated with pool price)
:= pool.nextTick (next initialized tick from current tick)
Verify specified price limit
Cases 1 & 4: MIN_SQRT_RATIO
< <
Cases 2 & 3: < < MAX_SQRT_RATIO
While specified amount not used up or price limit not reached,
Calculate temp next tick and next sqrt price . The temporary next tick is to ensure that the next tick does not exceed the MAX_TICK_DISTANCE cap from the current tick, so as not to violate the 5% price difference requirement.
= TickMath.getSqrtRatioAtTick()
Check if exceeds ,
If true then =
If false then =
Add collected swap fee to
If == , we are crossing tick :
Cross tick : updates the tick outside values and apply tick.liquidityNet to pool liquidity whilst fetching the next tick
Estimate , the swap fee to be collected by calling estimateIncrementalLiquidity()
calculate by calling calcIncrementalLiquidity()
Given L1, p1, fee and , calculate and p2
(1)
Finally calculate new
(2)
Given L1, p1 and p2 calculate the and
From (2) we have: combine with (1)
=>
=> (3)
Given L1, p1, fee and , calculate and p2
(1)
Finally calculate new
(2)
Given L1, p1 and p2 calculate the and
From (1) and (2)
=>
=> (3)
Given L1, p1 and p2, calculate the
=>
=>
=>
=>
Given L1, p1 and , calculate
=>
=>
=>
will be the smaller solution of this equation
Given L1, p1 and p2, calculate the
=>
=>
=>
=>
=>
Given L1, p1 and , calculate
=>
=>
=>