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 vecakeMembershipHook #9

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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 "pancake-v4-core/src/types/PoolKey.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "pancake-v4-core/src/types/BalanceDelta.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";

import {LPFeeLibrary} from "pancake-v4-core/src/libraries/LPFeeLibrary.sol";
import {Currency} from "pancake-v4-core/src/types/Currency.sol";
import {CurrencySettlement} from "pancake-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);
}
}
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/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 {CLPoolParametersHelper} from "pancake-v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {CLTestUtils} from "./utils/CLTestUtils.sol";
import {CLPoolParametersHelper} from "pancake-v4-core/src/pool-cl/libraries/CLPoolParametersHelper.sol";
import {PoolIdLibrary} from "pancake-v4-core/src/types/PoolId.sol";
import {ICLRouterBase} from "pancake-v4-periphery/src/pool-cl/interfaces/ICLRouterBase.sol";

import {VeCakeMembershipHook} from "../../src/pool-cl/VeCakeMembershipHook.sol";
import {LPFeeLibrary} from "pancake-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, 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);

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