Skip to content

Commit

Permalink
feat: User can increase liquidity after lock duration
Browse files Browse the repository at this point in the history
  • Loading branch information
ChefCupcake committed Dec 17, 2024
1 parent f646684 commit aa73e7d
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 6 deletions.
15 changes: 11 additions & 4 deletions src/pool-cl/anti-sniping/CLAntiSniping.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "pancake-v4-core/src/types
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";

import "forge-std/console.sol";
/// @title AntiSnipingHook
/// @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.
Expand Down Expand Up @@ -53,7 +53,7 @@ contract CLAntiSniping is CLBaseHook {

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

/// @notice Error thrown when attempting to partially withdraw from a position.
error PositionPartiallyWithdrawn();
Expand Down Expand Up @@ -167,10 +167,17 @@ contract CLAntiSniping is CLBaseHook {
liqParams.tickUpper = params.tickUpper;
liqParams.salt = params.salt;
positionKeyToLiquidityParams[positionKey] = liqParams;
if (positionCreationBlock[poolId][positionKey] != 0) revert PositionAlreadyExists();

if (positionCreationBlock[poolId][positionKey] != 0 &&
block.number - positionCreationBlock[poolId][positionKey] < positionLockDuration) {
revert PositionAlreadyExistsAndLocked();
}
if (positionsCreatedInLastBlock[poolId].length >= sameBlockPositionsLimit) revert TooManyPositionsInSameBlock();
if (positionCreationBlock[poolId][positionKey] == 0) {
positionsCreatedInLastBlock[poolId].push(positionKey);
}
positionCreationBlock[poolId][positionKey] = block.number;
positionsCreatedInLastBlock[poolId].push(positionKey);

return (this.beforeAddLiquidity.selector);
}

Expand Down
95 changes: 93 additions & 2 deletions test/pool-cl/CLAntiSniping.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ contract AntiSnipingTest is Test, Deployers, DeployPermit2 {
token1.transfer(CANDY, 10000 ether);
}

// Helper function to mint liquidity positions (add)
// Helper function to mint liquidity positions
function _mintLiquidityPosition(
address user,
int24 tickLower,
Expand All @@ -152,6 +152,32 @@ contract AntiSnipingTest is Test, Deployers, DeployPermit2 {
);
}

// Helper function to increase liquidity positions (add)
function _increaseLiquidityPosition(
address user,
uint256 tokenId,
uint256 liquidityDelta,
bytes memory hookData
) internal returns (uint256) {
vm.prank(user);
cpm.increaseLiquidity(
// tokenId:
tokenId,
// poolKey:
key,
// liquidity:
liquidityDelta,
// amount0Max:
10000e18,
// amount1Max:
10000e18,
// hookData:
hookData
);

return tokenId;
}

// Helper function to decrease liquidity positions (remove)
function _decreaseLiquidityPosition(
address user,
Expand All @@ -161,7 +187,7 @@ contract AntiSnipingTest is Test, Deployers, DeployPermit2 {
) internal {
vm.prank(user);
cpm.decreaseLiquidity(
// tokenId:
// tokenId:
tokenId,
// poolKey:
key,
Expand Down Expand Up @@ -365,6 +391,71 @@ contract AntiSnipingTest is Test, Deployers, DeployPermit2 {
);
}

/// @notice Test that swap after increase liquidity
function testSwapAfterIncreaseLiquidity() public {
// Record initial balances
uint256 aliceToken0Before = currency0.balanceOf(ALICE);
uint256 aliceToken1Before = currency1.balanceOf(ALICE);

aliceTokenId = _mintLiquidityPosition(ALICE, -60, 60, 10000 ether, ZERO_BYTES);

// Advance to next block
vm.roll(2);

// Attempt to add liquidity and expect revert
vm.expectRevert();
_increaseLiquidityPosition(ALICE, aliceTokenId, 10000 ether, ZERO_BYTES);

// Advance to after position lock duration
vm.roll(POSITION_LOCK_DURATION + 1);

// Alice add liquidity
_increaseLiquidityPosition(ALICE, aliceTokenId, 10000 ether, ZERO_BYTES);

// Swap occurs in the same block
uint128 swapAmount = 1 ether;
_performSwapExactOutputSingle(CANDY, true, swapAmount);

// Expected fees from swap
uint256 token0ExpectedFees = (uint256(swapAmount) * FEE) / 1e6; // Swap amount * fee percentage

// Advance to after position lock duration
vm.roll(POSITION_LOCK_DURATION + 3);
_performSwapExactOutputSingle(CANDY, false, swapAmount);
uint256 token1ExpectedFees = (uint256(swapAmount) * FEE) / 1e6;

// Collect fee info
PoolId poolId = key.toId();
antiSniping.collectLastBlockInfo(poolId);

// Calculate position keys
bytes32 alicePositionKey = CLPosition.calculatePositionKey(address(cpm), -60, 60, ALICE_SALT);

// Verify that Alice did not accrue fees in the creation block
assertEq(antiSniping.firstBlockFeesToken0(poolId, alicePositionKey), 0);
assertEq(antiSniping.firstBlockFeesToken1(poolId, alicePositionKey), 0);

// Attempt to remove liquidity and expect revert
vm.expectRevert();
_decreaseLiquidityPosition(ALICE, aliceTokenId, 20000 ether, ZERO_BYTES);

// Advance to after position lock duration
vm.roll(POSITION_LOCK_DURATION + POSITION_LOCK_DURATION + 2);

// Attempt to remove liquidity partially and expect revert
vm.expectRevert();
_decreaseLiquidityPosition(ALICE, aliceTokenId, 10000 ether, ZERO_BYTES);

// Alice removes liquidity
_decreaseLiquidityPosition(ALICE, aliceTokenId, 20000 ether, ZERO_BYTES);

// Verify that Alice received full fees from the first swap and half from the second
uint256 aliceToken0After = currency0.balanceOf(ALICE);
uint256 aliceToken1After = currency1.balanceOf(ALICE);
assertApproxEqAbsDecimal(aliceToken0After, aliceToken0Before + token0ExpectedFees, 1e15, 18);
assertApproxEqAbsDecimal(aliceToken1After, aliceToken1Before + token1ExpectedFees, 1e15, 18);
}

// --- Safeguard Tests ---

/// @notice Test that attempting to remove liquidity before lock duration reverts
Expand Down
17 changes: 17 additions & 0 deletions test/pool-cl/helpers/MockCLPositionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ contract MockCLPositionManager is CLPositionManager, CommonBase {
liquidityMinted = _getLiquidity(tokenId, poolKey, tickLower, tickUpper);
}

function increaseLiquidity(
uint256 tokenId,
PoolKey calldata poolKey,
uint256 liquidity,
uint128 amount0Max,
uint128 amount1Max,
bytes calldata hookData
) external payable {
Plan memory planner = Planner.init().add(
Actions.CL_INCREASE_LIQUIDITY, abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData)
);
bytes memory data = planner.finalizeModifyLiquidityWithClose(poolKey);

vm.prank(msg.sender);
this.modifyLiquidities(data, block.timestamp);
}

function decreaseLiquidity(
uint256 tokenId,
PoolKey calldata poolKey,
Expand Down

0 comments on commit aa73e7d

Please sign in to comment.