Skip to content
Open
91 changes: 91 additions & 0 deletions src/pool-cl/VeCakeSwapDiscountHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "pancake-v4-core/src/types/BeforeSwapDelta.sol";
import {PoolId, PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {ICLPoolManager} from "pancake-v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {CLBaseHook} from "./CLBaseHook.sol";

interface IVeCake {
function balanceOf(address account) external view returns (uint256 balance);
}

/// @notice VeCakeSwapDiscountHook provides 50% swap fee discount for veCake holder
/// Idea:
/// 1. PancakeSwap has veCake (vote-escrowed Cake), user obtain veCake by locking cake
/// 2. If the swapper holds veCake, provide 50% swap fee discount
/// Implementation:
/// 1. When pool is initialized, at `afterInitialize` we store what is the intended swap fee for the pool
// 2. During `beforeSwap` callback, the hook checks if users is veCake holder and provide discount accordingly
contract VeCakeSwapDiscountHook is CLBaseHook {
using PoolIdLibrary for PoolKey;
using LPFeeLibrary for uint24;

IVeCake public veCake;
mapping(PoolId => uint24) public poolIdToLpFee;

constructor(ICLPoolManager _poolManager, address _veCake) CLBaseHook(_poolManager) {
veCake = IVeCake(_veCake);
}

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

/// @notice The hook called after the state of a pool is initialized
/// @return bytes4 The function selector for the hook
function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData)
external
override
poolManagerOnly
returns (bytes4)
{
// Get the intended lpFee for this pool and store in mapping
uint24 lpFee = abi.decode(hookData, (uint24));
poolIdToLpFee[key.toId()] = lpFee;

return this.afterInitialize.selector;
}

/// @notice The hook called before a swap
/// @return bytes4 The function selector for the hook
/// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies.
/// @return uint24 Optionally override the lp fee, only used if three conditions are met:
/// 1) the Pool has a dynamic fee,
/// 2) the value's override flag is set to 1 i.e. vaule & OVERRIDE_FEE_FLAG = 0x400000 != 0
/// 3) the value is less than or equal to the maximum fee (1 million)
function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata)
external
override
poolManagerOnly
returns (bytes4, BeforeSwapDelta, uint24)
{
uint24 lpFee = poolIdToLpFee[key.toId()];

/// If veCake holder, lpFee is half
if (veCake.balanceOf(tx.origin) >= 1 ether) {
lpFee = lpFee / 2;
}

return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
}
96 changes: 96 additions & 0 deletions test/pool-cl/VeCakeSwapDiscountHook.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {Test} from "forge-std/Test.sol";
import {Constants} from "pancake-v4-core/test/pool-cl/helpers/Constants.sol";
import {Currency} from "pancake-v4-core/src/types/Currency.sol";
import {PoolKey} from "pancake-v4-core/src/types/PoolKey.sol";
import {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol";
import {CLPoolParametersHelper} from "pancake-v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {VeCakeSwapDiscountHook} from "../../src/pool-cl/VeCakeSwapDiscountHook.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {ICLRouterBase} from "pancake-v4-periphery/src/pool-cl/interfaces/ICLRouterBase.sol";

import {console2} from "forge-std/console2.sol";

contract VeCakeSwapDiscountHookTest is Test, CLTestUtils {
using PoolIdLibrary for PoolKey;
using CLPoolParametersHelper for bytes32;

VeCakeSwapDiscountHook hook;
Currency currency0;
Currency currency1;
PoolKey key;
MockERC20 veCake = new MockERC20("veCake", "veCake", 18);
address alice = makeAddr("alice");

function setUp() public {
(currency0, currency1) = deployContractsWithTokens();
hook = new VeCakeSwapDiscountHook(poolManager, address(veCake));

// create the pool key
key = PoolKey({
currency0: currency0,
currency1: currency1,
hooks: hook,
poolManager: poolManager,
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
parameters: bytes32(uint256(hook.getHooksRegistrationBitmap())).setTickSpacing(10)
});

// initialize pool at 1:1 price point and set 3000 (0.3%) as lp fee, lpFee is stored in the hook
poolManager.initialize(key, Constants.SQRT_RATIO_1_1, abi.encode(uint24(3000)));

// add liquidity so that swap can happen
MockERC20(Currency.unwrap(currency0)).mint(address(this), 100 ether);
MockERC20(Currency.unwrap(currency1)).mint(address(this), 100 ether);
addLiquidity(key, 100 ether, 100 ether, -60, 60, address(this));

// approve from alice for swap in the test cases below
permit2Approve(alice, currency0, address(universalRouter));
permit2Approve(alice, currency1, address(universalRouter));

// mint alice token for trade later
MockERC20(Currency.unwrap(currency0)).mint(address(alice), 100 ether);
}

function testNonVeCakeHolder() public {
uint256 amtOut = _swap();

// amt out be at least 0.3% lesser due to swap fee
assertLe(amtOut, 0.997 ether);
}

function testVeCakeHolder() public {
// mint alice veCake
veCake.mint(address(alice), 1 ether);

uint256 amtOut = _swap();

// amtOut should a bit lesser than 0.9985 due to 0.15% swap fee
assertLe(amtOut, 0.9985 ether);
assertGe(amtOut, 0.997 ether);
}

function _swap() internal returns (uint256 amtOut) {
uint256 amt1BalBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));

// set alice as tx.origin
vm.prank(address(alice), address(alice));
exactInputSingle(
ICLRouterBase.CLSwapExactInputSingleParams({
poolKey: key,
zeroForOne: true,
amountIn: 1 ether,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0,
hookData: new bytes(0)
})
);

uint256 amt1BalAfter = MockERC20(Currency.unwrap(currency1)).balanceOf(address(alice));
amtOut = amt1BalAfter - amt1BalBefore;
}
}