diff --git a/script/DeployPool.s.sol b/script/DeployPool.s.sol index ff60726..4c7a2c5 100644 --- a/script/DeployPool.s.sol +++ b/script/DeployPool.s.sol @@ -46,7 +46,7 @@ contract DeployPool is ScriptUtil { eulerAccount, uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG ), creationCode ); diff --git a/src/UniswapHook.sol b/src/UniswapHook.sol index 7b5ee18..0b04d06 100644 --- a/src/UniswapHook.sol +++ b/src/UniswapHook.sol @@ -27,8 +27,6 @@ contract UniswapHook is BaseHook { PoolKey internal _poolKey; - error AlreadyInitialized(); - error NativeConcentratedLiquidityUnsupported(); error LockedHook(); constructor(address evc_, address _poolManager) BaseHook(IPoolManager(_poolManager)) { @@ -143,25 +141,22 @@ contract UniswapHook is BaseHook { return (BaseHook.beforeSwap.selector, returnDelta, 0); } - /// @dev Each deployed hook only services one pair and prevent subsequent initializations - function _beforeInitialize(address, PoolKey calldata, uint160) internal view override returns (bytes4) { - // when the hook is deployed for the first time, the internal _poolKey is empty - // upon activation, the internal _poolKey is initialized and set - // once the hook contract is activated, do not allow subsequent initializations - require(_poolKey.tickSpacing == 0, AlreadyInitialized()); - return BaseHook.beforeInitialize.selector; - } - - function _beforeAddLiquidity(address, PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata) - internal - pure - override - returns (bytes4) - { - revert NativeConcentratedLiquidityUnsupported(); - } - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + /** + * @dev Hook Permissions without overrides: + * - beforeInitialize, beforeDoate, beforeAddLiquidity + * We use BaseHook's original reverts to *intentionally* revert + * + * beforeInitialize: the hook reverts for initializations NOT going through EulerSwap.activateHook() + * we want to prevent users from initializing other pairs with the same hook address + * + * beforeDonate: because the hook does not support native concentrated liquidity, any + * donations are permanently irrecoverable. The hook reverts on beforeDonate to prevent accidental misusage + * + * beforeAddLiquidity: the hook reverts to prevent v3-CLAMM positions + * because the hook is a "custom curve", any concentrated liquidity position sits idle and entirely unused + * to protect users from accidentally creating non-productive positions, the hook reverts on beforeAddLiquidity + */ return Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, @@ -171,7 +166,7 @@ contract UniswapHook is BaseHook { afterRemoveLiquidity: false, beforeSwap: true, afterSwap: false, - beforeDonate: false, + beforeDonate: true, afterDonate: false, beforeSwapReturnDelta: true, afterSwapReturnDelta: false, diff --git a/test/EulerSwapTestBase.t.sol b/test/EulerSwapTestBase.t.sol index 5c149f4..99a52a2 100644 --- a/test/EulerSwapTestBase.t.sol +++ b/test/EulerSwapTestBase.t.sol @@ -201,7 +201,7 @@ contract EulerSwapTestBase is EVaultTestBase { holder, uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG ), creationCode ); diff --git a/test/FactoryTest.t.sol b/test/FactoryTest.t.sol index 01ab94f..5b919db 100644 --- a/test/FactoryTest.t.sol +++ b/test/FactoryTest.t.sol @@ -35,7 +35,7 @@ contract FactoryTest is EulerSwapTestBase { function mineSalt(IEulerSwap.Params memory poolParams) internal view returns (address hookAddress, bytes32 salt) { uint160 flags = uint160( Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_DONATE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG ); bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); (hookAddress, salt) = HookMiner.find(address(eulerSwapFactory), holder, flags, creationCode); @@ -47,8 +47,10 @@ contract FactoryTest is EulerSwapTestBase { returns (address hookAddress, bytes32 salt) { // missing BEFORE_ADD_LIQUIDITY_FLAG - uint160 flags = - uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG); + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG + | Hooks.BEFORE_DONATE_FLAG + ); bytes memory creationCode = MetaProxyDeployer.creationCodeMetaProxy(eulerSwapImpl, abi.encode(poolParams)); (hookAddress, salt) = HookMiner.find(address(eulerSwapFactory), holder, flags, creationCode); } diff --git a/test/HookSwaps.t.sol b/test/HookSwaps.t.sol index ad28c4f..ffadea0 100644 --- a/test/HookSwaps.t.sol +++ b/test/HookSwaps.t.sol @@ -9,6 +9,7 @@ import {UniswapHook} from "../src/UniswapHook.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {IPoolManager, PoolManagerDeployer} from "./utils/PoolManagerDeployer.sol"; import {PoolSwapTest} from "@uniswap/v4-core/src/test/PoolSwapTest.sol"; +import {PoolDonateTest} from "@uniswap/v4-core/src/test/PoolDonateTest.sol"; import {MinimalRouter} from "./utils/MinimalRouter.sol"; import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiquidityTest.sol"; import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; @@ -18,6 +19,7 @@ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {CustomRevert} from "@uniswap/v4-core/src/libraries/CustomRevert.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol"; contract HookSwapsTest is EulerSwapTestBase { using StateLibrary for IPoolManager; @@ -28,6 +30,7 @@ contract HookSwapsTest is EulerSwapTestBase { PoolSwapTest public swapRouter; MinimalRouter public minimalRouter; PoolModifyLiquidityTest public liquidityManager; + PoolDonateTest public donateRouter; PoolSwapTest.TestSettings public settings = PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); @@ -38,6 +41,7 @@ contract HookSwapsTest is EulerSwapTestBase { swapRouter = new PoolSwapTest(poolManager); minimalRouter = new MinimalRouter(poolManager); liquidityManager = new PoolModifyLiquidityTest(poolManager); + donateRouter = new PoolDonateTest(poolManager); deployEulerSwap(address(poolManager)); @@ -133,13 +137,13 @@ contract HookSwapsTest is EulerSwapTestBase { assertTrue(perms.beforeAddLiquidity); assertTrue(perms.beforeSwap); assertTrue(perms.beforeSwapReturnDelta); + assertTrue(perms.beforeDonate); assertFalse(perms.afterInitialize); assertFalse(perms.afterAddLiquidity); assertFalse(perms.beforeRemoveLiquidity); assertFalse(perms.afterRemoveLiquidity); assertFalse(perms.afterSwap); - assertFalse(perms.beforeDonate); assertFalse(perms.afterDonate); assertFalse(perms.afterSwapReturnDelta); assertFalse(perms.afterAddLiquidityReturnDelta); @@ -163,7 +167,7 @@ contract HookSwapsTest is EulerSwapTestBase { CustomRevert.WrappedError.selector, address(eulerSwap), IHooks.beforeAddLiquidity.selector, - abi.encodeWithSelector(UniswapHook.NativeConcentratedLiquidityUnsupported.selector), + abi.encodeWithSelector(BaseHook.HookNotImplemented.selector), abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); @@ -186,13 +190,30 @@ contract HookSwapsTest is EulerSwapTestBase { CustomRevert.WrappedError.selector, address(eulerSwap), IHooks.beforeInitialize.selector, - abi.encodeWithSelector(UniswapHook.AlreadyInitialized.selector), + abi.encodeWithSelector(BaseHook.HookNotImplemented.selector), abi.encodeWithSelector(Hooks.HookCallFailed.selector) ) ); poolManager.initialize(newPoolKey, 79228162514264337593543950336); } + /// @dev revert on donations as they are irrecoverable if they were supported + function test_revertDonate(uint256 amount0, uint256 amount1) public { + PoolKey memory poolKey = eulerSwap.poolKey(); + + // hook intentionally reverts to prevent irrecoverable donations + vm.expectRevert( + abi.encodeWithSelector( + CustomRevert.WrappedError.selector, + address(eulerSwap), + IHooks.beforeDonate.selector, + abi.encodeWithSelector(BaseHook.HookNotImplemented.selector), + abi.encodeWithSelector(Hooks.HookCallFailed.selector) + ) + ); + donateRouter.donate(poolKey, amount0, amount1, ""); + } + function _swap(PoolKey memory key, bool zeroForOne, bool exactInput, uint256 amount) internal { IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({ zeroForOne: zeroForOne,