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

[Do not merge] Showcase example hooks #4

Closed
wants to merge 10 commits into from
111 changes: 111 additions & 0 deletions src/pool-cl/VeCakeMembershipHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "@pancakeswap/v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@pancakeswap/v4-core/src/types/BeforeSwapDelta.sol";
import {PoolId, PoolIdLibrary} from "@pancakeswap/v4-core/src/types/PoolId.sol";
import {ICLPoolManager} from "@pancakeswap/v4-core/src/pool-cl/interfaces/ICLPoolManager.sol";
import {CLBaseHook} from "./CLBaseHook.sol";

import {LPFeeLibrary} from "@pancakeswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {Currency} from "@pancakeswap/v4-core/src/types/Currency.sol";
import {CurrencySettlement} from "@pancakeswap/v4-core/test/helpers/CurrencySettlement.sol";

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

/// @notice VeCakeMembershipHook provides the following features for veCake holders:
/// 1. veCake holder will get 0% swap fee for the first hour
/// 2. veCake holder will get 5% more tokenOut when swap exactIn token0 for token1 subsidised by hook
contract VeCakeMembershipHook is CLBaseHook {
using CurrencySettlement for Currency;
using PoolIdLibrary for PoolKey;

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

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: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnsDelta: false,
afterSwapReturnsDelta: true,
afterAddLiquidityReturnsDelta: false,
afterRemoveLiquidityReturnsDelta: false
})
);
}

function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData)
external
override
returns (bytes4)
{
uint24 swapFee = abi.decode(hookData, (uint24));
poolIdToLpFee[key.toId()] = swapFee;

promoEndDate = block.timestamp + 1 hours;
return this.afterInitialize.selector;
}

function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata)
external
view
override
poolManagerOnly
returns (bytes4, BeforeSwapDelta, uint24)
{
// return early if promo has ended
if (block.timestamp > promoEndDate) {
return (
this.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
poolIdToLpFee[key.toId()] | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}

uint24 lpFee = veCake.balanceOf(tx.origin) >= 1 ether ? 0 : poolIdToLpFee[key.toId()];
return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}

function afterSwap(
address,
PoolKey calldata key,
ICLPoolManager.SwapParams calldata param,
BalanceDelta delta,
bytes calldata
) external override poolManagerOnly returns (bytes4, int128) {
// return early if promo has ended
if (block.timestamp > promoEndDate) {
return (this.afterSwap.selector, 0);
}

// param.amountSpecified < 0 implies exactIn
if (param.zeroForOne && param.amountSpecified < 0 && veCake.balanceOf(tx.origin) >= 1 ether) {
// delta.amount1 is positive as zeroForOne
int128 extraToken = delta.amount1() * 5 / 100;

// settle and return negative value to indicate that hook is giving token
key.currency1.settle(vault, address(this), uint128(extraToken), false);
return (this.afterSwap.selector, -extraToken);
}

return (this.afterSwap.selector, 0);
}
}
86 changes: 86 additions & 0 deletions src/pool-cl/VeCakeSwapDiscountHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.sol";
import {LPFeeLibrary} from "@pancakeswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@pancakeswap/v4-core/src/types/BeforeSwapDelta.sol";
import {PoolId, PoolIdLibrary} from "@pancakeswap/v4-core/src/types/PoolId.sol";
import {ICLPoolManager} from "@pancakeswap/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
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)
{
/// If veCake holder, lpFee is half
uint24 lpFee = veCake.balanceOf(tx.origin) < 1 ether ? poolIdToLpFee[key.toId()] : poolIdToLpFee[key.toId()] / 2;

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

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

import {VeCakeMembershipHook} from "../../src/pool-cl/VeCakeMembershipHook.sol";
import {LPFeeLibrary} from "@pancakeswap/v4-core/src/libraries/LPFeeLibrary.sol";

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

VeCakeMembershipHook 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 VeCakeMembershipHook(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 as initial 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);

// approve from alice for swap in the test cases below
vm.startPrank(alice);
MockERC20(Currency.unwrap(currency0)).approve(address(swapRouter), type(uint256).max);
MockERC20(Currency.unwrap(currency1)).approve(address(swapRouter), type(uint256).max);
vm.stopPrank();

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

// mint currency 1 for hook to give out
MockERC20(Currency.unwrap(currency1)).mint(address(hook), 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_AfterPromoPeriod() public {
vm.warp(hook.promoEndDate() + 1);

// mint alice veCake
veCake.mint(address(alice), 1 ether);

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();

// amount out is almost 1.05 due to the 5% subsidy from hook and 0% swap fee
assertGt(amtOut, 1.04 ether);
}

function _swap() internal returns (uint256 amtOut) {
// set alice as tx.origin
vm.prank(address(alice), address(alice));

amtOut = swapRouter.exactInputSingle(
ICLSwapRouterBase.V4CLExactInputSingleParams({
poolKey: key,
zeroForOne: true,
recipient: address(alice),
amountIn: 1 ether,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0,
hookData: new bytes(0)
}),
block.timestamp
);
}
}
Loading