-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
549 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
} | ||
} |
Oops, something went wrong.