Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add anti-sniping hook contract #20

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions src/pool-cl/anti-sniping/CLAntiSniping.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// SPDX-License-Identifier: UNLICENSED
// Copyright (C) 2024 PancakeSwap
pragma solidity ^0.8.19;

import {CLBaseHook} from "../CLBaseHook.sol";
import {IPoolManager} from "pancake-v4-core/src/interfaces/IPoolManager.sol";
import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {Tick} from "pancake-v4-core/src/pool-cl/libraries/Tick.sol";
import {Hooks} from "pancake-v4-core/src/libraries/Hooks.sol";
import {CLPosition} from "pancake-v4-core/src/pool-cl/libraries/CLPosition.sol";
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "pancake-v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "pancake-v4-core/src/types/BeforeSwapDelta.sol";
import {FullMath} from "pancake-v4-core/src/pool-cl/libraries/FullMath.sol";
import {FixedPoint128} from "pancake-v4-core/src/pool-cl/libraries/FixedPoint128.sol";
import {SafeCast} from "pancake-v4-core/src/libraries/SafeCast.sol";

/// @title AntiSnipingHook
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a disclaimer here?

 * @dev Disclaimer:
 *      - This contract has not been audited.
 *      - Developers using this code are advised to thoroughly review and test it before deploying it to production.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, added

/// @notice A PancakeSwap V4 hook that prevents MEV sniping attacks by enforcing time locks on positions and redistributing fees accrued in the initial block to legitimate liquidity providers.
/// @dev Positions are time-locked, and fees accrued in the first block after position creation are redistributed.
contract CLAntiSniping is CLBaseHook {
using PoolIdLibrary for PoolKey;
using SafeCast for *;

/// @notice Maps a pool ID and position key to the block number when the position was created.
mapping(PoolId => mapping(bytes32 => uint256)) public positionCreationBlock;

/// @notice The duration (in blocks) for which a position must remain locked before it can be removed.
uint128 public positionLockDuration;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can make this immutable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, change to immutable


/// @notice The maximum number of positions that can be created in the same block per pool to prevent excessive gas usage.
uint128 public sameBlockPositionsLimit;
ChefCupcake marked this conversation as resolved.
Show resolved Hide resolved

mapping(PoolId => uint256) lastProcessedBlockNumber;

mapping(PoolId => bytes32[]) positionsCreatedInLastBlock;

struct LiquidityParams {
int24 tickLower;
int24 tickUpper;
bytes32 salt;
address sender;
}
mapping(bytes32 => LiquidityParams) positionKeyToLiquidityParams;

/// @notice Maps a pool ID and position key to the fees accrued in the first block.
mapping(PoolId => mapping(bytes32 => uint256)) public firstBlockFeesToken0;
mapping(PoolId => mapping(bytes32 => uint256)) public firstBlockFeesToken1;

/// @notice Error thrown when a position is still locked and cannot be removed.
error PositionLocked();

/// @notice Error thrown when attempting to modify an existing position.
/// @dev Positions cannot be modified after creation to prevent edge cases.
error PositionAlreadyExists();

/// @notice Error thrown when attempting to partially withdraw from a position.
error PositionPartiallyWithdrawn();

/// @notice Error thrown when too many positions are opened in the same block.
/// @dev Limits the number of positions per block to prevent excessive gas consumption.
error TooManyPositionsInSameBlock();

constructor(ICLPoolManager poolManager, uint128 _positionLockDuration, uint128 _sameBlockPositionsLimit)
CLBaseHook(poolManager)
{
positionLockDuration = _positionLockDuration;
sameBlockPositionsLimit = _sameBlockPositionsLimit;
}

function getHooksRegistrationBitmap() external pure override returns (uint16) {
return _hooksRegistrationBitmapFrom(
Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: true,
beforeSwap: true,
afterSwap: false,
beforeDonate: true,
afterDonate: false,
beforeSwapReturnsDelta: false,
afterSwapReturnsDelta: false,
afterAddLiquidiyReturnsDelta: false,
afterRemoveLiquidiyReturnsDelta: true
})
);
}

/// @notice Collects fee information for positions created in the last processed block.
/// @dev This is called in all of the before hooks (except init) and can also be called manually.
/// @param poolId The identifier of the pool.
function collectLastBlockInfo(PoolId poolId) public {
if (block.number <= lastProcessedBlockNumber[poolId]) {
return;
}
lastProcessedBlockNumber[poolId] = block.number;
for (uint256 i = 0; i < positionsCreatedInLastBlock[poolId].length; i++) {
bytes32 positionKey = positionsCreatedInLastBlock[poolId][i];
LiquidityParams memory params = positionKeyToLiquidityParams[positionKey];
CLPosition.Info memory info = poolManager.getPosition(poolId, params.sender, params.tickLower, params.tickUpper, params.salt);
(uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = _getFeeGrowthInside(poolId, params.sender, params.tickLower, params.tickUpper, params.salt);
uint256 feeGrowthDelta0X128 = feeGrowthInside0X128 - info.feeGrowthInside0LastX128;
uint256 feeGrowthDelta1X128 = feeGrowthInside1X128 - info.feeGrowthInside1LastX128;
firstBlockFeesToken0[poolId][positionKey] =
ChefCupcake marked this conversation as resolved.
Show resolved Hide resolved
FullMath.mulDiv(feeGrowthDelta0X128, info.liquidity, FixedPoint128.Q128);
firstBlockFeesToken1[poolId][positionKey] =
FullMath.mulDiv(feeGrowthDelta1X128, info.liquidity, FixedPoint128.Q128);
}
delete positionsCreatedInLastBlock[poolId];
}

/// @notice Handles logic after removing liquidity, redistributing first-block fees if applicable.
/// @dev Donates first-block accrued fees to the pool if liquidity remains; otherwise, returns them to the sender.
function afterRemoveLiquidity(
address sender,
PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata params,
BalanceDelta,
BalanceDelta,
bytes calldata
) external override returns (bytes4, BalanceDelta) {
PoolId poolId = key.toId();
bytes32 positionKey = CLPosition.calculatePositionKey(sender, params.tickLower, params.tickUpper, params.salt);

BalanceDelta hookDelta;
if (poolManager.getLiquidity(poolId) != 0) {
hookDelta = toBalanceDelta(
firstBlockFeesToken0[poolId][positionKey].toInt128(),
firstBlockFeesToken1[poolId][positionKey].toInt128()
ChefCupcake marked this conversation as resolved.
Show resolved Hide resolved
);
poolManager.donate(
key, firstBlockFeesToken0[poolId][positionKey], firstBlockFeesToken1[poolId][positionKey], new bytes(0)
);
} else {
// If the pool is empty, the fees are not donated and are returned to the sender
hookDelta = BalanceDeltaLibrary.ZERO_DELTA;
}
return (this.afterRemoveLiquidity.selector, hookDelta);
Copy link
Collaborator

@ChefMist ChefMist Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why we are not doing the Cleanup stored data for the position?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here
maybe it's fine to not clean up if we are assuming that all users will only use position manager to add/removeLiq (position keys will always be different due to the fact that salt=tokenId)
but what if the user is not using position manager?
also as a plus, delete can save gas costs

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because only mint and remove all liquidity are allowed, so tokenId will not be repeat in this scenario for extra gas costs,but yes i can add delete in this func

}

/// @notice Handles logic before adding liquidity, enforcing position creation constraints.
/// @dev Records position creation block and ensures the position doesn't already exist or exceed the same block limit.
function beforeAddLiquidity(
address sender,
PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata params,
bytes calldata
) external override returns (bytes4) {
PoolId poolId = key.toId();
collectLastBlockInfo(poolId);
bytes32 positionKey = CLPosition.calculatePositionKey(sender, params.tickLower, params.tickUpper, params.salt);
LiquidityParams storage liqParams = positionKeyToLiquidityParams[positionKey];
ChefCupcake marked this conversation as resolved.
Show resolved Hide resolved
liqParams.sender = sender;
liqParams.tickLower = params.tickLower;
liqParams.tickUpper = params.tickUpper;
liqParams.salt = params.salt;
positionKeyToLiquidityParams[positionKey] = liqParams;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

liqParams is storage , do not need this line.
positionKeyToLiquidityParams[positionKey] = liqParams;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, removed

if (positionCreationBlock[poolId][positionKey] != 0) revert PositionAlreadyExists();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ppl can not add liquidity into an existing position ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can, but it will prevent not be sniping attack by MEV in swap and donate

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how @ChefCupcake ? doesn't this condition blocks user from adding liquidity to the same position?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and what happens if user add liquidity via CLPositionManager?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in raw project it is not allow add, only mint, but i will try to open the restrict, and test if it is ok

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea same question here, I don't think users can still add liquidity to the same position

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i think the raw project not allow add reason is the hook need to remember the timestamp when the liquidity was created, otherwise the timestamp will be last added, then it will conflict with the lock duration

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ChefCupcake does this means user cannot increase liquidity for the same tokenId? (yes or no)?

if the answer is yes, please prep a doc on some suggestions on how we can fix this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the answer is yes, if user increase liquidity, then the timestamp should the last action, the firstBlockFeesToken0 and firstBlockFeesToken1 will be lost, beside as the hook doesn't allow partial remove liquidity, so it will extend the lock duration

if (positionsCreatedInLastBlock[poolId].length >= sameBlockPositionsLimit) revert TooManyPositionsInSameBlock();
positionCreationBlock[poolId][positionKey] = block.number;
positionsCreatedInLastBlock[poolId].push(positionKey);
return (this.beforeAddLiquidity.selector);
}

/// @notice Handles logic before removing liquidity, enforcing position lock duration and full withdrawal.
/// @dev Checks that the position lock duration has passed and disallows partial withdrawals.
function beforeRemoveLiquidity(
address sender,
PoolKey calldata key,
ICLPoolManager.ModifyLiquidityParams calldata params,
bytes calldata
) external override returns (bytes4) {
PoolId poolId = key.toId();
collectLastBlockInfo(poolId);
bytes32 positionKey = CLPosition.calculatePositionKey(sender, params.tickLower, params.tickUpper, params.salt);
if (block.number - positionCreationBlock[poolId][positionKey] < positionLockDuration) revert PositionLocked();
CLPosition.Info memory info = poolManager.getPosition(poolId, sender, params.tickLower, params.tickUpper, params.salt);
if (int128(info.liquidity) + params.liquidityDelta != 0) revert PositionPartiallyWithdrawn();
return (this.beforeRemoveLiquidity.selector);
}

/// @notice Handles logic before a swap occurs.
/// @dev Collects fee information for positions created in the last processed block.
function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata)
external
override
returns (bytes4, BeforeSwapDelta, uint24)
{
PoolId poolId = key.toId();
collectLastBlockInfo(poolId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can u add some test cases so that we can compare the gas snapshot ? I am a bit curious that how many gas overhead will this function bring

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, will add testcase later

return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

/// @notice Handles logic before a donation occurs.
/// @dev Collects fee information for positions created in the last processed block.
function beforeDonate(address, PoolKey calldata key, uint256, uint256, bytes calldata)
external
override
returns (bytes4)
{
PoolId poolId = key.toId();
collectLastBlockInfo(poolId);
return (this.beforeDonate.selector);
}

function _getFeeGrowthInside(
PoolId poolId,
address owner,
int24 tickLower,
int24 tickUpper,
bytes32 salt
) internal view returns (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) {
(, int24 tickCurrent,,) = poolManager.getSlot0(poolId);
Tick.Info memory lower = poolManager.getPoolTickInfo(poolId, tickLower);
Tick.Info memory upper = poolManager.getPoolTickInfo(poolId, tickUpper);

(uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = poolManager.getFeeGrowthGlobals(poolId);

// calculate fee growth below
uint256 feeGrowthBelow0X128;
uint256 feeGrowthBelow1X128;

unchecked {
if (tickCurrent >= tickLower) {
feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
} else {
feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
}

// calculate fee growth above
uint256 feeGrowthAbove0X128;
uint256 feeGrowthAbove1X128;
if (tickCurrent < tickUpper) {
feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
} else {
feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
}

feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}
}
}
Loading