AntiSniping Mechanism
Protecting Against FrontRunners
Introduction
As more value started to accrue towards DeFi, a new opportunity for value extraction arose based on how orders were prioritized on a public blockchain. As it takes time for a transaction to be finalized (i.e. added to a block), front runners could capitalize on the pending transaction whose information was publicly available. This practice came to be known as Maximal Extractable Value (MEV) and more details can be found here.
Specific to AMMs, sniping is a specific form of MEV whereby an attacker jumps in front of normal liquidity providers by adding and removing liquidity just before and right after a huge swap. To protect our LPs, KyberSwap Elastic comes with an antisniping feature to natively protect them from any potential frontrunners.
Motivation
New AMM, which compounds different positions such as Uniswap v3, provides tools for liquidity providers to add and remove liquidity easily in specific ranges. The feature leads to a novel attack, called sniping, where the attacker tries to jump in front of normal liquidity providers by adding and removing liquidity just before and right after a huge swap.
We have collected some data to show the impact of the attack.
The sandwich attack can be effectively restrained on the taker's side thanks to parameters limiting how much slippage can be tolerated. However, there is no similar antisniping attack mechanism for liquidity providers. Hence, the protocol should introduce a feature that protects liquidity providers from this type of attack.
KyberSwap Elastic's AntiSniping Feature is introduced as a lock of reward, which is vested based on the duration of liquidity contribution. The principal difference between attacks and normal activities of liquidity providers is their contribution duration. When liquidity providers supply their funds to the protocol, they take the risk of impermanent loss. However, in the case of an attacker who withdraws their fund immediately, the impermanent loss can be precalculated so that their profit is guaranteed.
Mechanism
Struct: Data
Field  Type  Explanation 


 timestamp of last action performed 

 average start time of lock schedule 

 average unlock time of locked fees 

 locked rToken qty since last update 
Adding liquidity
When adding liquidity to a position, it updates Data values for that position and calculates the amount of claimable reinvestment tokens to be sent to the user.
The values are calculates based on the unlockTime, lockedTime, lastActionTime, currentTime, feesLocked and feeSinceLastAction, the formula can be found here.
Removing liquidity
When removing liquidity from a position, it updates Data values for that position and calculates the amount of claimable reinvestment tokens, as well as the amount of reinvestment tokens to be burnt.
After calculating the claimable amount of fees, the burnt fees is calculated based on the amount of liquidity that user is withdrawing.
Whitelisting PositionManager
As we implement the AntiSniping Attack mechanism in the PositionMananger contract, we need to prevent users/contracts from interacting directly with Pool contracts. To do so, we allow only whitelisted PositionManager contracts to call the mint function.
We don't have the whitelisted check in the burn function because:
Only the PositionManager holds positions in the Pool contracts, thus, users must interact with PositionManager to burn their positions.
In case we upgrade to a new PositionManager contract, all previous PositionManager contracts should still be able to remove liquidity.
The Factory contract stores all whitelisted PositionManager contracts, and have a function called isWhitelistedNFTManager(address) for Pool contracts to check if an address is whitelisted. To make it flexible, the Factory can disable the whitelisting feature, i.e: the isWhitelistedNFTManager function will always return true.
Example
Let's take an example where we have a pool of Token0/Token1 with 3 positions and 5 ticks and the current price (Token0/Token1) is 1. The relationship between price and tick is:
Swapping exact input of Token0 (amount NOT enough to cross tick)
The next tick is 1 (t_tmp(1)=1). Calculate square root of price to cross the next tick. We will divide into two functions next if the price moves rightward else back if it moves leftward.
Calculate delta_x_tmp that we need to cross the next tick
We see that delta_x_tmp > delta_x so we do not cross any tick. Now we calculate the collected liquidity l_c:
And calculate the new sqrtPrice:
Then we calculate delta_y which is the amount of output in token1.
The delta_y is negative because this is the amount of token1 that taker will take out of the liquidity pool. Now we can update the current price of liquidity pool equal to the new sqrtPrice.
And the collected liquidity is added to the reinvestment liquidity:
Swap exact output of Token1 (amount enough to cross tick)
The next tick is 1 (t_tmp(1)=1). Calculate square root of price to cross the next tick. We will divide into two functions next if the price moves rightward else back if it moves leftward.
Calculate delta_y_tmp that we need to cross the next tick
We see that delta_y_tmp < delta_y so we cross the next tick. Now we calculate the collected liquidity l_c:
Then we can update the delta_x:
Now we calculate the rest amount of \Delta_y need to be swapped:
Now we calculate the additional pool tokens will be minted:
So we will update the total supply of the reinvestment tokens:
Now we calculate the sqrt price of the next step:
When we cross to tick 2 we must also update liquidity of the pool  l_p_tmp. It will subtract the liquidity net of the tick. In this simple example, so l_p_tmp=l_p=16, meanwhile the temporary liquidity of the reinvestment curve l_f_tmp=l_f+l_c=3.00000142711 Calculate delta_y_tmp that we need to cross the next tick
So delta_y_tmp>delta_y_r, we do not cross one more tick. Then we move to next step to calculate the new amount of collected fee.
And calculate the new sqrtPrice:
Then we calculate delta_x which is the amount of input in token0.
Now we calculate the additional pool tokens will be minted:
So we will update the total supply of the reinvestment tokens:
Now we can update the current price of the liquidity pool equal to the new sqrtPrice.
And the collected liquidity is added to the reinvestment liquidity:
Add liquidity
Liquidity provider add liquidity to the pool to price range from 0.99995000375 to 1.00004999875 (tick 1 to tick 1), which means the current price is in the price range of the the position, the amount of liquidity to be added:
Now we need to calculate delta_x, delta_y taken from user:
When adding liquidity to a position, it updates data values for that position and calculates the amount of claimable reinvestment tokens to be sent to the user. In this case, we consider that there is a different between the last time balances of reinvestment curve has been updated. The new reinvestment liquidity l_f 3.1 meanwhile the last time we updated the balances, the reinvestment liquidity l_f_last is 3. Now we calculate the additional pool tokens will be minted:
So we will update the total supply of the reinvestment tokens:
We also need to update the balances of every liquidity providers and the "last" reinvestment liquidity (l_f_last). Then we transfer the collected pool tokens from addressPool to the user if the amount of reinvestment tokens collected of the user is greater than 0.
Remove liquidity
Similar to add liquidity when removing liquidity from a position, it updates data values for that position and calculates the amount of claimable reinvestment tokens, as well as the amount of reinvestment tokens to be burnt. After calculating the claimable amount of fees, the burnt fees is calculated based on the amount of liquidity that user is withdrawing.
Last updated