Skip to content

Commit

Permalink
example hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
ChefMist committed Jun 19, 2024
1 parent b56ae5b commit 54e31f2
Show file tree
Hide file tree
Showing 6 changed files with 549 additions and 0 deletions.
110 changes: 110 additions & 0 deletions src/pool-cl/VeCakeMembershipHook.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 {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.sol";
import {LPFeeLibrary} from "@pancakeswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {BalanceDelta} 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 {Currency} from "@pancakeswap/v4-core/src/types/Currency.sol";
import {CurrencySettlement} from "@pancakeswap/v4-core/test/helpers/CurrencySettlement.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 VeCakeMembershipHook provides the following features for veCake holders:
/// 1. veCake holder will get 5% more tokenOut subsidised by hook
/// 2. veCake holder get 100% off swap fee for the first hour
contract VeCakeMembershipHook is CLBaseHook {
using CurrencySettlement for Currency;
using PoolIdLibrary for PoolKey;
using LPFeeLibrary for uint24;

error PoolNotOpenForPublicTradeYet();

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

constructor(ICLPoolManager _poolManager, address _veCake) CLBaseHook(_poolManager) {
veCake = IVeCake(_veCake);
promoEndDate = block.timestamp + 1 hours;
}

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

/// @dev Get the intended lpFee for this pool and store in mapping
function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData)
external
override
returns (bytes4)
{
(uint24 lpFee) = abi.decode(hookData, (uint24));
poolIdToLpFee[key.toId()] = lpFee;

return this.afterInitialize.selector;
}

function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata)
external
override
poolManagerOnly
returns (bytes4, BeforeSwapDelta, uint24)
{
/// If within promo endDate and veCake holder, lpFee is 0
uint24 lpFee =
block.timestamp < promoEndDate && 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 poolKey,
ICLPoolManager.SwapParams calldata param,
BalanceDelta delta,
bytes calldata
) external override returns (bytes4, int128) {

// return early if promo has ended
if (block.timestamp > promoEndDate) {
return (this.afterSwap.selector, 0);
}

/// @dev this is POC code, do not use for production!!
/// Assumption: currency1 is subsidised currency and if veCake user swap token0 for token1, give 5% more token1.
if (param.zeroForOne && 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
poolKey.currency1.settle(vault, address(this), uint128(extraToken), false);
return (this.afterSwap.selector, -extraToken);
}

return (this.afterSwap.selector, 0);
}
}
73 changes: 73 additions & 0 deletions src/pool-cl/VeCakeSwapDiscountHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 allows veCake to get 50% off swap fee
contract VeCakeSwapDiscountHook is CLBaseHook {
using PoolIdLibrary for PoolKey;
using LPFeeLibrary for uint24;

error PoolNotOpenForPublicTradeYet();

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

/// @dev Get the intended lpFee for this pool and store in mapping
function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata hookData)
external
override
returns (bytes4)
{
uint24 lpFee = abi.decode(hookData, (uint24));
poolIdToLpFee[key.toId()] = lpFee;

return this.afterInitialize.selector;
}

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);
}
}
66 changes: 66 additions & 0 deletions src/pool-cl/VeCakeWhitelistHook.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import {PoolKey} from "@pancakeswap/v4-core/src/types/PoolKey.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 {console2} from "forge-std/console2.sol";

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

/// @notice VeCakeWhitelistHook allows only veCake holder to trade with the pool within the first hour
contract VeCakeWhitelistHook is CLBaseHook {
using PoolIdLibrary for PoolKey;

error PoolNotOpenForPublicTradeYet();

IVeCake veCake;

// The time when public trade starts, before this, only veCake holder can trade
uint256 public publicTradeStartTime;

constructor(ICLPoolManager _poolManager, address _veCake) CLBaseHook(_poolManager) {
veCake = IVeCake(_veCake);
publicTradeStartTime = block.timestamp + 1 hours;
}

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

function beforeSwap(address, PoolKey calldata key, ICLPoolManager.SwapParams calldata, bytes calldata)
external
override
poolManagerOnly
returns (bytes4, BeforeSwapDelta, uint24)
{
/// Only allow non veCake holder to trade after publicTradeStartTime
if (block.timestamp < publicTradeStartTime && veCake.balanceOf(tx.origin) < 1 ether) {
revert PoolNotOpenForPublicTradeYet();
}

return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
}
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 {LPFeeLibrary} from "@pancakeswap/v4-core/src/libraries/LPFeeLibrary.sol";
import {CLPoolParametersHelper} from "@pancakeswap/v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {VeCakeMembershipHook} from "../../src/pool-cl/VeCakeMembershipHook.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {PoolIdLibrary} from "@pancakeswap/v4-core/src/types/PoolId.sol";
import {ICLSwapRouterBase} from "pancake-v4-periphery/src/pool-cl/interfaces/ICLSwapRouterBase.sol";

import {console2} from "forge-std/console2.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 testVeCakeHolderX() public {
// mint alice veCake
veCake.mint(address(alice), 1 ether);

uint256 amtOut = _swap();

// amount out is more than amtIn to indicate hook has given some extra tokenOut
assertGt(amtOut, 1 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

0 comments on commit 54e31f2

Please sign in to comment.