diff --git a/.forge-snapshots/before swap hook, already cached dynamic fee.snap b/.forge-snapshots/before swap hook, already cached dynamic fee.snap index de1d92de3..c56739307 100644 --- a/.forge-snapshots/before swap hook, already cached dynamic fee.snap +++ b/.forge-snapshots/before swap hook, already cached dynamic fee.snap @@ -1 +1 @@ -190294 \ No newline at end of file +191802 \ No newline at end of file diff --git a/.forge-snapshots/cached dynamic fee, no hooks.snap b/.forge-snapshots/cached dynamic fee, no hooks.snap index 008c2a36f..d2c3192c0 100644 --- a/.forge-snapshots/cached dynamic fee, no hooks.snap +++ b/.forge-snapshots/cached dynamic fee, no hooks.snap @@ -1 +1 @@ -145654 \ No newline at end of file +147163 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 1 token.snap b/.forge-snapshots/donate gas with 1 token.snap index 07f524502..8e17d4459 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -137259 \ No newline at end of file +138487 \ No newline at end of file diff --git a/.forge-snapshots/donate gas with 2 tokens.snap b/.forge-snapshots/donate gas with 2 tokens.snap index fbb2d5b96..446ee3d38 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -186980 \ No newline at end of file +185645 \ No newline at end of file diff --git a/.forge-snapshots/erc20 collect protocol fees.snap b/.forge-snapshots/erc20 collect protocol fees.snap index f6af67bf2..6489f5191 100644 --- a/.forge-snapshots/erc20 collect protocol fees.snap +++ b/.forge-snapshots/erc20 collect protocol fees.snap @@ -1 +1 @@ -27033 \ No newline at end of file +26991 \ No newline at end of file diff --git a/.forge-snapshots/gas overhead of no-op lock.snap b/.forge-snapshots/gas overhead of no-op lock.snap index 7bc046e9a..0f2325b21 100644 --- a/.forge-snapshots/gas overhead of no-op lock.snap +++ b/.forge-snapshots/gas overhead of no-op lock.snap @@ -1 +1 @@ -14918 \ No newline at end of file +14940 \ No newline at end of file diff --git a/.forge-snapshots/initialize.snap b/.forge-snapshots/initialize.snap index 937fa842e..544626c4f 100644 --- a/.forge-snapshots/initialize.snap +++ b/.forge-snapshots/initialize.snap @@ -1 +1 @@ -75025 \ No newline at end of file +77663 \ No newline at end of file diff --git a/.forge-snapshots/mint with empty hook.snap b/.forge-snapshots/mint with empty hook.snap index dfe339245..ae79eadb0 100644 --- a/.forge-snapshots/mint with empty hook.snap +++ b/.forge-snapshots/mint with empty hook.snap @@ -1 +1 @@ -319543 \ No newline at end of file +318152 \ No newline at end of file diff --git a/.forge-snapshots/mint with native token.snap b/.forge-snapshots/mint with native token.snap index c19db5d28..65306fd8c 100644 --- a/.forge-snapshots/mint with native token.snap +++ b/.forge-snapshots/mint with native token.snap @@ -1 +1 @@ -199129 \ No newline at end of file +200911 \ No newline at end of file diff --git a/.forge-snapshots/mint.snap b/.forge-snapshots/mint.snap index dbf2490ae..1c55c6e6e 100644 --- a/.forge-snapshots/mint.snap +++ b/.forge-snapshots/mint.snap @@ -1 +1 @@ -202244 \ No newline at end of file +200853 \ No newline at end of file diff --git a/.forge-snapshots/modify position with noop.snap b/.forge-snapshots/modify position with noop.snap index 9c02936e1..bec78271c 100644 --- a/.forge-snapshots/modify position with noop.snap +++ b/.forge-snapshots/modify position with noop.snap @@ -1 +1 @@ -52336 \ No newline at end of file +56127 \ No newline at end of file diff --git a/.forge-snapshots/native collect protocol fees.snap b/.forge-snapshots/native collect protocol fees.snap index 5a35b1041..d71adf7a4 100644 --- a/.forge-snapshots/native collect protocol fees.snap +++ b/.forge-snapshots/native collect protocol fees.snap @@ -1 +1 @@ -38699 \ No newline at end of file +38657 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index 4a9e16fb4..67f49b024 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -25078 \ No newline at end of file +25255 \ No newline at end of file diff --git a/.forge-snapshots/simple swap with native.snap b/.forge-snapshots/simple swap with native.snap index e239e3ff8..679164dd8 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -191062 \ No newline at end of file +196049 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index a8dc5de6a..233a133ea 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -202765 \ No newline at end of file +204617 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity with native token.snap b/.forge-snapshots/swap against liquidity with native token.snap index bf74e846c..138766b90 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -121658 \ No newline at end of file +126645 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 1e8955e5f..ede40397b 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -109483 \ No newline at end of file +114135 \ No newline at end of file diff --git a/.forge-snapshots/swap burn claim for input.snap b/.forge-snapshots/swap burn claim for input.snap index 2f8711a95..756c39036 100644 --- a/.forge-snapshots/swap burn claim for input.snap +++ b/.forge-snapshots/swap burn claim for input.snap @@ -1 +1 @@ -128222 \ No newline at end of file +133204 \ No newline at end of file diff --git a/.forge-snapshots/swap mint output as claim.snap b/.forge-snapshots/swap mint output as claim.snap index d69e090b9..7261415f1 100644 --- a/.forge-snapshots/swap mint output as claim.snap +++ b/.forge-snapshots/swap mint output as claim.snap @@ -1 +1 @@ -212934 \ No newline at end of file +216787 \ No newline at end of file diff --git a/.forge-snapshots/swap with dynamic fee.snap b/.forge-snapshots/swap with dynamic fee.snap index 9d2e2ca2b..cebfa0d84 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -189534 \ No newline at end of file +191043 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 0222993d0..ced84b452 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -109462 \ No newline at end of file +114114 \ No newline at end of file diff --git a/.forge-snapshots/swap with noop.snap b/.forge-snapshots/swap with noop.snap index 6caa9ae17..6ea826b4e 100644 --- a/.forge-snapshots/swap with noop.snap +++ b/.forge-snapshots/swap with noop.snap @@ -1 +1 @@ -45202 \ No newline at end of file +48947 \ No newline at end of file diff --git a/.forge-snapshots/update dynamic fee in before swap.snap b/.forge-snapshots/update dynamic fee in before swap.snap index b6ddfb21c..a690ab9be 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -196118 \ No newline at end of file +197626 \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index 2b58ecbcf..6d27cc06f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 2b58ecbcf3dfde7a75959dc7b4eb3d0670278de6 +Subproject commit 6d27cc06fb2668965cbbc533d6ac3385c91a19a3 diff --git a/src/PoolManager.sol b/src/PoolManager.sol index b8fa49bfb..85222f1d8 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -90,6 +90,18 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { return Lockers.getLocker(i); } + /// @notice This will revert if a function is called by any address other than the current locker OR the most recently called, pre-permissioned hook. + modifier onlyByLocker() { + _checkLocker(msg.sender, Lockers.getCurrentLocker(), Lockers.getCurrentHook()); + _; + } + + function _checkLocker(address caller, address locker, IHooks hook) internal pure { + if (caller == locker) return; + if (caller == address(hook) && hook.hasPermissionToAccessLock()) return; + revert LockedBy(locker, address(hook)); + } + /// @inheritdoc IPoolManager function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) external @@ -105,6 +117,8 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual(); if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks)); + (bool set) = Lockers.setCurrentHook(key.hooks); + if (key.hooks.shouldCallBeforeInitialize()) { if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector) { @@ -127,6 +141,9 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { } } + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + // On intitalize we emit the key's fee, which tells us all fee settings a pool can have: either a static swap fee or dynamic swap fee and if the hook has enabled swap or withdraw fees. emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks); } @@ -174,26 +191,27 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { if (pools[id].isNotInitialized()) revert PoolNotInitialized(); } - modifier onlyByLocker() { - address locker = Lockers.getCurrentLocker(); - if (msg.sender != locker) revert LockedBy(locker); - _; - } - /// @inheritdoc IPoolManager function modifyPosition( PoolKey memory key, IPoolManager.ModifyPositionParams memory params, bytes calldata hookData ) external override noDelegateCall onlyByLocker returns (BalanceDelta delta) { + (bool set) = Lockers.setCurrentHook(key.hooks); + PoolId id = key.toId(); _checkPoolInitialized(id); if (key.hooks.shouldCallBeforeModifyPosition()) { bytes4 selector = key.hooks.beforeModifyPosition(msg.sender, key, params, hookData); // Sentinel return value used to signify that a NoOp occurred. - if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA; - else if (selector != IHooks.beforeModifyPosition.selector) revert Hooks.InvalidHookResponse(); + if (key.hooks.isValidNoOpCall(selector)) { + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + return BalanceDeltaLibrary.MAXIMUM_DELTA; + } else if (selector != IHooks.beforeModifyPosition.selector) { + revert Hooks.InvalidHookResponse(); + } } Pool.FeeAmounts memory feeAmounts; @@ -233,6 +251,9 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { } } + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + emit ModifyPosition(id, msg.sender, params.tickLower, params.tickUpper, params.liquidityDelta); } @@ -244,14 +265,21 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { onlyByLocker returns (BalanceDelta delta) { + (bool set) = Lockers.setCurrentHook(key.hooks); + PoolId id = key.toId(); _checkPoolInitialized(id); if (key.hooks.shouldCallBeforeSwap()) { bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData); // Sentinel return value used to signify that a NoOp occurred. - if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA; - else if (selector != IHooks.beforeSwap.selector) revert Hooks.InvalidHookResponse(); + if (key.hooks.isValidNoOpCall(selector)) { + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + return BalanceDeltaLibrary.MAXIMUM_DELTA; + } else if (selector != IHooks.beforeSwap.selector) { + revert Hooks.InvalidHookResponse(); + } } uint256 feeForProtocol; @@ -285,6 +313,9 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { } } + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + emit Swap( id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee ); @@ -298,14 +329,21 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { onlyByLocker returns (BalanceDelta delta) { + (bool set) = Lockers.setCurrentHook(key.hooks); + PoolId id = key.toId(); _checkPoolInitialized(id); if (key.hooks.shouldCallBeforeDonate()) { bytes4 selector = key.hooks.beforeDonate(msg.sender, key, amount0, amount1, hookData); // Sentinel return value used to signify that a NoOp occurred. - if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA; - else if (selector != IHooks.beforeDonate.selector) revert Hooks.InvalidHookResponse(); + if (key.hooks.isValidNoOpCall(selector)) { + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); + return BalanceDeltaLibrary.MAXIMUM_DELTA; + } else if (selector != IHooks.beforeDonate.selector) { + revert Hooks.InvalidHookResponse(); + } } delta = pools[id].donate(amount0, amount1); @@ -317,6 +355,9 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { revert Hooks.InvalidHookResponse(); } } + + // We only want to clear the current hook if it was set in setCurrentHook in this execution frame. + if (set) Lockers.clearCurrentHook(); } /// @inheritdoc IPoolManager @@ -400,6 +441,10 @@ contract PoolManager is IPoolManager, Fees, NoDelegateCall, Claims { return Lockers.nonzeroDeltaCount(); } + function getCurrentHook() external view returns (IHooks) { + return Lockers.getCurrentHook(); + } + /// @notice receive native tokens for native pools receive() external payable {} } diff --git a/src/interfaces/IPoolManager.sol b/src/interfaces/IPoolManager.sol index e9b974bf5..b7a6199e1 100644 --- a/src/interfaces/IPoolManager.sol +++ b/src/interfaces/IPoolManager.sol @@ -23,7 +23,8 @@ interface IPoolManager is IFees, IClaims { /// @notice Thrown when a function is called by an address that is not the current locker /// @param locker The current locker - error LockedBy(address locker); + /// @param currentHook The most recently called hook + error LockedBy(address locker, address currentHook); /// @notice The ERC1155 being deposited is not the Uniswap ERC1155 error NotPoolManagerToken(); @@ -123,6 +124,9 @@ interface IPoolManager is IFees, IClaims { /// @notice Returns the length of the lockers array, which is the number of locks open on the PoolManager. function getLockLength() external view returns (uint256 _length); + /// @notice Returns the most recently called hook. + function getCurrentHook() external view returns (IHooks _currentHook); + /// @notice Returns the number of nonzero deltas open on the PoolManager that must be zerod by the close of the initial lock. function getLockNonzeroDeltaCount() external view returns (uint256 _nonzeroDeltaCount); diff --git a/src/libraries/Hooks.sol b/src/libraries/Hooks.sol index 3710c739f..3577a4934 100644 --- a/src/libraries/Hooks.sol +++ b/src/libraries/Hooks.sol @@ -20,10 +20,11 @@ library Hooks { uint256 internal constant BEFORE_DONATE_FLAG = 1 << 153; uint256 internal constant AFTER_DONATE_FLAG = 1 << 152; uint256 internal constant NO_OP_FLAG = 1 << 151; + uint256 internal constant ACCESS_LOCK_FLAG = 1 << 150; bytes4 public constant NO_OP_SELECTOR = bytes4(keccak256(abi.encodePacked("NoOp"))); - struct Calls { + struct Permissions { bool beforeInitialize; bool afterInitialize; bool beforeModifyPosition; @@ -33,6 +34,7 @@ library Hooks { bool beforeDonate; bool afterDonate; bool noOp; + bool accessLock; } /// @notice Thrown if the address will not lead to the specified hook calls being called @@ -44,17 +46,19 @@ library Hooks { /// @notice Utility function intended to be used in hook constructors to ensure /// the deployed hooks address causes the intended hooks to be called - /// @param calls The hooks that are intended to be called - /// @dev calls param is memory as the function will be called from constructors - function validateHookAddress(IHooks self, Calls memory calls) internal pure { + /// @param permissions The hooks that are intended to be called + /// @dev permissions param is memory as the function will be called from constructors + function validateHookPermissions(IHooks self, Permissions memory permissions) internal pure { if ( - calls.beforeInitialize != shouldCallBeforeInitialize(self) - || calls.afterInitialize != shouldCallAfterInitialize(self) - || calls.beforeModifyPosition != shouldCallBeforeModifyPosition(self) - || calls.afterModifyPosition != shouldCallAfterModifyPosition(self) - || calls.beforeSwap != shouldCallBeforeSwap(self) || calls.afterSwap != shouldCallAfterSwap(self) - || calls.beforeDonate != shouldCallBeforeDonate(self) || calls.afterDonate != shouldCallAfterDonate(self) - || calls.noOp != hasPermissionToNoOp(self) + permissions.beforeInitialize != shouldCallBeforeInitialize(self) + || permissions.afterInitialize != shouldCallAfterInitialize(self) + || permissions.beforeModifyPosition != shouldCallBeforeModifyPosition(self) + || permissions.afterModifyPosition != shouldCallAfterModifyPosition(self) + || permissions.beforeSwap != shouldCallBeforeSwap(self) + || permissions.afterSwap != shouldCallAfterSwap(self) + || permissions.beforeDonate != shouldCallBeforeDonate(self) + || permissions.afterDonate != shouldCallAfterDonate(self) || permissions.noOp != hasPermissionToNoOp(self) + || permissions.accessLock != hasPermissionToAccessLock(self) ) { revert HookAddressNotValid(address(self)); } @@ -74,7 +78,7 @@ library Hooks { return address(hook) == address(0) ? !fee.isDynamicFee() && !fee.hasHookSwapFee() && !fee.hasHookWithdrawFee() : ( - uint160(address(hook)) >= AFTER_DONATE_FLAG || fee.isDynamicFee() || fee.hasHookSwapFee() + uint160(address(hook)) >= ACCESS_LOCK_FLAG || fee.isDynamicFee() || fee.hasHookSwapFee() || fee.hasHookWithdrawFee() ); } @@ -111,6 +115,10 @@ library Hooks { return uint256(uint160(address(self))) & AFTER_DONATE_FLAG != 0; } + function hasPermissionToAccessLock(IHooks self) internal pure returns (bool) { + return uint256(uint160(address(self))) & ACCESS_LOCK_FLAG != 0; + } + function hasPermissionToNoOp(IHooks self) internal pure returns (bool) { return uint256(uint160(address(self))) & NO_OP_FLAG != 0; } diff --git a/src/libraries/Lockers.sol b/src/libraries/Lockers.sol index c9929f6c1..575dfa067 100644 --- a/src/libraries/Lockers.sol +++ b/src/libraries/Lockers.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.20; +import {IHooks} from "../interfaces/IHooks.sol"; + /// @notice This is a temporary library that allows us to use transient storage (tstore/tload) /// for the lockers array and nonzero delta count. /// TODO: This library can be deleted when we have the transient keyword support in solidity. @@ -8,6 +10,9 @@ library Lockers { // The starting slot for an array of lockers, stored transiently. uint256 constant LOCKERS_SLOT = uint256(keccak256("Lockers")) - 1; + // The starting slot for an array of hook addresses per locker, stored transiently. + uint256 constant HOOK_ADDRESS_SLOT = uint256(keccak256("HookAddress")) - 1; + // The slot holding the number of nonzero deltas. uint256 constant NONZERO_DELTA_COUNT = uint256(keccak256("NonzeroDeltaCount")) - 1; @@ -94,4 +99,34 @@ library Lockers { tstore(slot, count) } } + + function getCurrentHook() internal view returns (IHooks currentHook) { + return IHooks(getHook(length())); + } + + function getHook(uint256 i) internal view returns (address hook) { + uint256 slot = HOOK_ADDRESS_SLOT + i; + assembly { + hook := tload(slot) + } + } + + function setCurrentHook(IHooks currentHook) internal returns (bool set) { + // Set the hook address for the current locker if the address is 0. + // If the address is nonzero, a hook has already been set for this lock, and is not allowed to be updated or cleared at the end of the call. + if (address(getCurrentHook()) == address(0)) { + uint256 slot = HOOK_ADDRESS_SLOT + length(); + assembly { + tstore(slot, currentHook) + } + return true; + } + } + + function clearCurrentHook() internal { + uint256 slot = HOOK_ADDRESS_SLOT + length(); + assembly { + tstore(slot, 0) + } + } } diff --git a/src/test/AccessLockHook.sol b/src/test/AccessLockHook.sol new file mode 100644 index 000000000..35fb60650 --- /dev/null +++ b/src/test/AccessLockHook.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {BaseTestHooks} from "./BaseTestHooks.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; +import {CurrencyLibrary, Currency} from "../types/Currency.sol"; +import {Hooks} from "../libraries/Hooks.sol"; +import {TickMath} from "../libraries/TickMath.sol"; +import {Test} from "forge-std/Test.sol"; +import {ILockCallback} from "../interfaces/callback/ILockCallback.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {Constants} from "../../test/utils/Constants.sol"; +import {PoolIdLibrary} from "../types/PoolId.sol"; + +contract AccessLockHook is Test, BaseTestHooks { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + IPoolManager manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + error InvalidAction(); + + enum LockAction { + Mint, + Take, + Donate, + Swap, + ModifyPosition, + Burn, + Settle, + Initialize, + NoOp + } + + function beforeInitialize( + address, /* sender **/ + PoolKey calldata key, + uint160, /* sqrtPriceX96 **/ + bytes calldata hookData + ) external override returns (bytes4) { + return _executeAction(key, hookData, IHooks.beforeInitialize.selector); + } + + function beforeSwap( + address, /* sender **/ + PoolKey calldata key, + IPoolManager.SwapParams calldata, /* params **/ + bytes calldata hookData + ) external override returns (bytes4) { + return _executeAction(key, hookData, IHooks.beforeSwap.selector); + } + + function beforeDonate( + address, /* sender **/ + PoolKey calldata key, + uint256, /* amount0 **/ + uint256, /* amount1 **/ + bytes calldata hookData + ) external override returns (bytes4) { + return _executeAction(key, hookData, IHooks.beforeDonate.selector); + } + + function beforeModifyPosition( + address, /* sender **/ + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata, /* params **/ + bytes calldata hookData + ) external override returns (bytes4) { + return _executeAction(key, hookData, IHooks.beforeModifyPosition.selector); + } + + function _executeAction(PoolKey memory key, bytes calldata hookData, bytes4 selector) internal returns (bytes4) { + if (hookData.length == 0) { + // We have re-entered the hook or we are initializing liquidity in the pool before testing the lock actions. + return selector; + } + (uint256 amount, LockAction action) = abi.decode(hookData, (uint256, LockAction)); + + // These actions just use some hardcoded parameters. + if (action == LockAction.Mint) { + manager.mint(key.currency1, address(this), amount); + } else if (action == LockAction.Take) { + manager.take(key.currency1, address(this), amount); + } else if (action == LockAction.Donate) { + manager.donate(key, amount, amount, new bytes(0)); + } else if (action == LockAction.Swap) { + manager.swap( + key, + IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: int256(amount), + sqrtPriceLimitX96: TickMath.MIN_SQRT_RATIO + 1 + }), + new bytes(0) + ); + } else if (action == LockAction.ModifyPosition) { + manager.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -60, tickUpper: 60, liquidityDelta: int256(amount)}), + new bytes(0) + ); + } else if (action == LockAction.NoOp) { + assertEq(address(manager.getCurrentHook()), address(this)); + return Hooks.NO_OP_SELECTOR; + } else if (action == LockAction.Burn) { + manager.burn(key.currency1, amount); + } else if (action == LockAction.Settle) { + manager.take(key.currency1, address(this), amount); + assertEq(MockERC20(Currency.unwrap(key.currency1)).balanceOf(address(this)), amount); + assertEq(manager.getLockNonzeroDeltaCount(), 1); + MockERC20(Currency.unwrap(key.currency1)).transfer(address(manager), amount); + manager.settle(key.currency1); + assertEq(manager.getLockNonzeroDeltaCount(), 0); + } else if (action == LockAction.Initialize) { + PoolKey memory newKey = PoolKey({ + currency0: key.currency0, + currency1: key.currency1, + fee: Constants.FEE_LOW, + tickSpacing: 60, + hooks: IHooks(address(0)) + }); + manager.initialize(newKey, Constants.SQRT_RATIO_1_2, new bytes(0)); + } else { + revert InvalidAction(); + } + + return selector; + } +} + +// Hook that can access the lock. +// Also has the ability to call out to another hook or pool. +contract AccessLockHook2 is Test, BaseTestHooks { + IPoolManager manager; + + error IncorrectHookSet(); + + constructor(IPoolManager _manager) { + manager = _manager; + } + + function beforeModifyPosition( + address sender, + PoolKey calldata key, + IPoolManager.ModifyPositionParams calldata params, + bytes calldata hookData + ) external override returns (bytes4) { + if (address(manager.getCurrentHook()) != address(this)) { + revert IncorrectHookSet(); + } + + (bool shouldCallHook, PoolKey memory key2) = abi.decode(hookData, (bool, PoolKey)); + + if (shouldCallHook) { + // Should revert. + bytes memory hookData2 = abi.encode(100, AccessLockHook.LockAction.Mint); + IHooks(key2.hooks).beforeModifyPosition(sender, key, params, hookData2); // params dont really matter, just want to tell the other hook to do a mint action, but will revert + } else { + // Should succeed and should NOT set the current hook to key2.hooks. + // The permissions should remain to THIS hook during this lock. + manager.modifyPosition(key2, params, new bytes(0)); + + if (address(manager.getCurrentHook()) != address(this)) { + revert IncorrectHookSet(); + } + // Should succeed. + manager.mint(key.currency1, address(this), 10); + } + return IHooks.beforeModifyPosition.selector; + } +} + +// Reenters the PoolManager to donate and asserts currentHook is set and unset correctly throughout the popping and pushing of locks. +contract AccessLockHook3 is Test, ILockCallback, BaseTestHooks { + IPoolManager manager; + // The pool to donate to in the nested lock. + // Ensure this has balance of currency0.abi + PoolKey key; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + // Instead of passing through key all the way to the nested lock, just save it. + function setKey(PoolKey memory _key) external { + key = _key; + } + + function beforeModifyPosition( + address, /* sender **/ + PoolKey calldata, /* key **/ + IPoolManager.ModifyPositionParams calldata, /* params **/ + bytes calldata /* hookData **/ + ) external override returns (bytes4) { + assertEq(address(manager.getCurrentHook()), address(this)); + manager.lock(abi.encode(true)); + assertEq(address(manager.getCurrentHook()), address(this)); + manager.lock(abi.encode(false)); + assertEq(address(manager.getCurrentHook()), address(this)); + return IHooks.beforeModifyPosition.selector; + } + + function lockAcquired(bytes memory data) external returns (bytes memory) { + assertEq(manager.getLockLength(), 2); + assertEq(address(manager.getCurrentHook()), address(0)); + + (bool isFirstLock) = abi.decode(data, (bool)); + if (isFirstLock) { + manager.donate(key, 10, 0, new bytes(0)); + assertEq(address(manager.getCurrentHook()), address(key.hooks)); + MockERC20(Currency.unwrap(key.currency0)).transfer(address(manager), 10); + manager.settle(key.currency0); + } + return data; + } +} diff --git a/src/test/EmptyTestHooks.sol b/src/test/EmptyTestHooks.sol index 254e62ff3..6e8f9fc3f 100644 --- a/src/test/EmptyTestHooks.sol +++ b/src/test/EmptyTestHooks.sol @@ -11,8 +11,8 @@ contract EmptyTestHooks is IHooks { using Hooks for IHooks; constructor() { - IHooks(this).validateHookAddress( - Hooks.Calls({ + IHooks(this).validateHookPermissions( + Hooks.Permissions({ beforeInitialize: true, afterInitialize: true, beforeModifyPosition: true, @@ -21,7 +21,8 @@ contract EmptyTestHooks is IHooks { afterSwap: true, beforeDonate: true, afterDonate: true, - noOp: true + noOp: true, + accessLock: true }) ); } diff --git a/src/test/HooksTest.sol b/src/test/HooksTest.sol index 460ed9a53..a99ec9e9b 100644 --- a/src/test/HooksTest.sol +++ b/src/test/HooksTest.sol @@ -7,8 +7,8 @@ import {IHooks} from "../interfaces/IHooks.sol"; contract HooksTest { using Hooks for IHooks; - function validateHookAddress(address hookAddress, Hooks.Calls calldata params) external pure { - IHooks(hookAddress).validateHookAddress(params); + function validateHookPermissions(address hookAddress, Hooks.Permissions calldata params) external pure { + IHooks(hookAddress).validateHookPermissions(params); } function isValidHookAddress(address hookAddress, uint24 fee) external pure returns (bool) { @@ -53,13 +53,13 @@ contract HooksTest { return gasBefore - gasleft(); } - function getGasCostOfValidateHookAddress(address hookAddress, Hooks.Calls calldata params) + function getGasCostOfValidateHookAddress(address hookAddress, Hooks.Permissions calldata params) external view returns (uint256) { uint256 gasBefore = gasleft(); - IHooks(hookAddress).validateHookAddress(params); + IHooks(hookAddress).validateHookPermissions(params); return gasBefore - gasleft(); } } diff --git a/src/test/NoOpTestHooks.sol b/src/test/NoOpTestHooks.sol index aeed7c532..5c5dd8a1d 100644 --- a/src/test/NoOpTestHooks.sol +++ b/src/test/NoOpTestHooks.sol @@ -9,9 +9,9 @@ import {BalanceDelta} from "../types/BalanceDelta.sol"; contract NoOpTestHooks is BaseTestHooks { constructor() { - Hooks.validateHookAddress( + Hooks.validateHookPermissions( this, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: true, @@ -20,7 +20,8 @@ contract NoOpTestHooks is BaseTestHooks { afterSwap: false, beforeDonate: true, afterDonate: false, - noOp: true + noOp: true, + accessLock: false }) ); } diff --git a/src/test/PoolDonateTest.sol b/src/test/PoolDonateTest.sol index 464d53f46..bc14ebd06 100644 --- a/src/test/PoolDonateTest.sol +++ b/src/test/PoolDonateTest.sol @@ -52,11 +52,20 @@ contract PoolDonateTest is PoolTestBase, Test { BalanceDelta delta = manager.donate(data.key, data.amount0, data.amount1, data.hookData); + // Checks that the current hook is cleared if there is an access lock. Note that if this router is ever used in a nested lock this will fail. + assertEq(address(manager.getCurrentHook()), address(0)); + (,, uint256 reserveAfter0, int256 deltaAfter0) = _fetchBalances(data.key.currency0, data.sender); (,, uint256 reserveAfter1, int256 deltaAfter1) = _fetchBalances(data.key.currency1, data.sender); - assertEq(reserveBefore0, reserveAfter0); - assertEq(reserveBefore1, reserveAfter1); + if (!data.key.hooks.hasPermissionToAccessLock()) { + assertEq(reserveBefore0, reserveAfter0); + assertEq(reserveBefore1, reserveAfter1); + if (!data.key.hooks.hasPermissionToNoOp()) { + assertEq(deltaAfter0, int256(data.amount0)); + assertEq(deltaAfter1, int256(data.amount1)); + } + } if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { // Check that this hook is allowed to NoOp, then we can return as we dont need to settle @@ -64,11 +73,10 @@ contract PoolDonateTest is PoolTestBase, Test { return abi.encode(delta); } - assertEq(deltaAfter0, int256(data.amount0)); - assertEq(deltaAfter1, int256(data.amount1)); - - if (data.amount0 > 0) _settle(data.key.currency0, data.sender, delta.amount0(), true); - if (data.amount1 > 0) _settle(data.key.currency1, data.sender, delta.amount1(), true); + if (deltaAfter0 > 0) _settle(data.key.currency0, data.sender, int128(deltaAfter0), true); + if (deltaAfter1 > 0) _settle(data.key.currency1, data.sender, int128(deltaAfter1), true); + if (deltaAfter0 < 0) _take(data.key.currency0, data.sender, int128(deltaAfter0), true); + if (deltaAfter1 < 0) _take(data.key.currency1, data.sender, int128(deltaAfter1), true); return abi.encode(delta); } diff --git a/src/test/PoolInitializeTest.sol b/src/test/PoolInitializeTest.sol index 9e67d7182..c0bcc32f6 100644 --- a/src/test/PoolInitializeTest.sol +++ b/src/test/PoolInitializeTest.sol @@ -7,10 +7,13 @@ import {PoolKey} from "../types/PoolKey.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; import {Test} from "forge-std/Test.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; +import {Hooks} from "../libraries/Hooks.sol"; contract PoolInitializeTest is Test, PoolTestBase { using CurrencyLibrary for Currency; using SafeCast for uint256; + using Hooks for IHooks; constructor(IPoolManager _manager) PoolTestBase(_manager) {} @@ -18,13 +21,14 @@ contract PoolInitializeTest is Test, PoolTestBase { PoolKey key; uint160 sqrtPriceX96; bytes hookData; + address sender; } function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes memory hookData) external returns (int24 tick) { - tick = abi.decode(manager.lock(abi.encode(CallbackData(key, sqrtPriceX96, hookData))), (int24)); + tick = abi.decode(manager.lock(abi.encode(CallbackData(key, sqrtPriceX96, hookData, msg.sender))), (int24)); } function lockAcquired(bytes calldata rawData) external returns (bytes memory) { @@ -38,9 +42,17 @@ contract PoolInitializeTest is Test, PoolTestBase { int256 delta1 = manager.currencyDelta(address(this), data.key.currency1); uint256 nonZeroDC = manager.getLockNonzeroDeltaCount(); - assertEq(delta0, 0, "delta0"); - assertEq(delta1, 0, "delta1"); - assertEq(nonZeroDC, 0, "NonzeroDeltaCount"); + if (!data.key.hooks.hasPermissionToAccessLock()) { + assertEq(delta0, 0, "delta0"); + assertEq(delta1, 0, "delta1"); + assertEq(nonZeroDC, 0, "NonzeroDeltaCount"); + } else { + // settle deltas + if (delta0 > 0) _settle(data.key.currency0, data.sender, int128(delta0), true); + if (delta1 > 0) _settle(data.key.currency1, data.sender, int128(delta1), true); + if (delta0 < 0) _take(data.key.currency0, data.sender, int128(delta0), true); + if (delta1 < 0) _take(data.key.currency1, data.sender, int128(delta1), true); + } return abi.encode(tick); } diff --git a/src/test/PoolModifyPositionTest.sol b/src/test/PoolModifyPositionTest.sol index acdc4737c..a25925f2c 100644 --- a/src/test/PoolModifyPositionTest.sol +++ b/src/test/PoolModifyPositionTest.sol @@ -44,22 +44,28 @@ contract PoolModifyPositionTest is Test, PoolTestBase { CallbackData memory data = abi.decode(rawData, (CallbackData)); BalanceDelta delta = manager.modifyPosition(data.key, data.params, data.hookData); + // Checks that the current hook is cleared if there is an access lock. Note that if this router is ever used in a nested lock this will fail. + assertEq(address(manager.getCurrentHook()), address(0)); (,,, int256 delta0) = _fetchBalances(data.key.currency0, data.sender); (,,, int256 delta1) = _fetchBalances(data.key.currency1, data.sender); - if (data.params.liquidityDelta > 0) { - assert(delta0 > 0 || delta1 > 0 || data.key.hooks.hasPermissionToNoOp()); - assert(!(delta0 < 0 || delta1 < 0)); - if (delta0 > 0) _settle(data.key.currency0, data.sender, delta.amount0(), true); - if (delta1 > 0) _settle(data.key.currency1, data.sender, delta.amount1(), true); - } else { - assert(delta0 < 0 || delta1 < 0 || data.key.hooks.hasPermissionToNoOp()); - assert(!(delta0 > 0 || delta1 > 0)); - if (delta0 < 0) _take(data.key.currency0, data.sender, delta.amount0(), true); - if (delta1 < 0) _take(data.key.currency1, data.sender, delta.amount1(), true); + // These assertions only apply in non lock-accessing pools. + if (!data.key.hooks.hasPermissionToAccessLock()) { + if (data.params.liquidityDelta > 0) { + assert(delta0 > 0 || delta1 > 0 || data.key.hooks.hasPermissionToNoOp()); + assert(!(delta0 < 0 || delta1 < 0)); + } else { + assert(delta0 < 0 || delta1 < 0 || data.key.hooks.hasPermissionToNoOp()); + assert(!(delta0 > 0 || delta1 > 0)); + } } + if (delta0 > 0) _settle(data.key.currency0, data.sender, int128(delta0), true); + if (delta1 > 0) _settle(data.key.currency1, data.sender, int128(delta1), true); + if (delta0 < 0) _take(data.key.currency0, data.sender, int128(delta0), true); + if (delta1 < 0) _take(data.key.currency1, data.sender, int128(delta1), true); + return abi.encode(delta); } } diff --git a/src/test/PoolSwapTest.sol b/src/test/PoolSwapTest.sol index 99b3060e3..c5fe0ab04 100644 --- a/src/test/PoolSwapTest.sol +++ b/src/test/PoolSwapTest.sol @@ -9,6 +9,8 @@ import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; import {Test} from "forge-std/Test.sol"; +import {Hooks} from "../libraries/Hooks.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; contract PoolSwapTest is Test, PoolTestBase { using CurrencyLibrary for Currency; @@ -58,14 +60,42 @@ contract PoolSwapTest is Test, PoolTestBase { BalanceDelta delta = manager.swap(data.key, data.params, data.hookData); + // Checks that the current hook is cleared if there is an access lock. Note that if this router is ever used in a nested lock this will fail. + assertEq(address(manager.getCurrentHook()), address(0)); + (,, uint256 reserveAfter0, int256 deltaAfter0) = _fetchBalances(data.key.currency0, data.sender); (,, uint256 reserveAfter1, int256 deltaAfter1) = _fetchBalances(data.key.currency1, data.sender); - // Make sure youve added liquidity to the test pool! - if (BalanceDelta.unwrap(delta) == 0) revert NoSwapOccurred(); - - assertEq(reserveBefore0, reserveAfter0); - assertEq(reserveBefore1, reserveAfter1); + if (!data.key.hooks.hasPermissionToAccessLock()) { + // Hanndle assertions when the hook cannot access the lock. + // IE if the hook can access the lock, the reserves before and after are not necessarily the same. Hook can "take". + assertEq(reserveBefore0, reserveAfter0); + assertEq(reserveBefore1, reserveAfter1); + + if (!data.key.hooks.hasPermissionToNoOp()) { + if (data.params.zeroForOne) { + if (data.params.amountSpecified > 0) { + // exact input, 0 for 1 + assertEq(deltaAfter0, data.params.amountSpecified); + assert(deltaAfter1 < 0); + } else { + // exact output, 0 for 1 + assert(deltaAfter0 > 0); + assertEq(deltaAfter1, data.params.amountSpecified); + } + } else { + if (data.params.amountSpecified > 0) { + // exact input, 1 for 0 + assertEq(deltaAfter1, data.params.amountSpecified); + assert(deltaAfter0 < 0); + } else { + // exact output, 1 for 0 + assert(deltaAfter1 > 0); + assertEq(deltaAfter0, data.params.amountSpecified); + } + } + } + } if (delta == BalanceDeltaLibrary.MAXIMUM_DELTA) { // Check that this hook is allowed to NoOp, then we can return as we dont need to settle @@ -73,30 +103,17 @@ contract PoolSwapTest is Test, PoolTestBase { return abi.encode(delta); } - if (data.params.zeroForOne) { - if (data.params.amountSpecified > 0) { - // exact input, 0 for 1 - assertEq(deltaAfter0, data.params.amountSpecified); - assert(deltaAfter1 < 0); - } else { - // exact output, 0 for 1 - assert(deltaAfter0 > 0); - assertEq(deltaAfter1, data.params.amountSpecified); - } - _settle(data.key.currency0, data.sender, delta.amount0(), data.testSettings.settleUsingTransfer); - _take(data.key.currency1, data.sender, delta.amount1(), data.testSettings.withdrawTokens); - } else { - if (data.params.amountSpecified > 0) { - // exact input, 1 for 0 - assertEq(deltaAfter1, data.params.amountSpecified); - assert(deltaAfter0 < 0); - } else { - // exact output, 1 for 0 - assert(deltaAfter1 > 0); - assertEq(deltaAfter0, data.params.amountSpecified); - } - _settle(data.key.currency1, data.sender, delta.amount1(), data.testSettings.settleUsingTransfer); - _take(data.key.currency0, data.sender, delta.amount0(), data.testSettings.withdrawTokens); + if (deltaAfter0 > 0) { + _settle(data.key.currency0, data.sender, int128(deltaAfter0), data.testSettings.settleUsingTransfer); + } + if (deltaAfter1 > 0) { + _settle(data.key.currency1, data.sender, int128(deltaAfter1), data.testSettings.settleUsingTransfer); + } + if (deltaAfter0 < 0) { + _take(data.key.currency0, data.sender, int128(deltaAfter0), data.testSettings.withdrawTokens); + } + if (deltaAfter1 < 0) { + _take(data.key.currency1, data.sender, int128(deltaAfter1), data.testSettings.withdrawTokens); } return abi.encode(delta); diff --git a/src/test/PoolTestBase.sol b/src/test/PoolTestBase.sol index 2bf1257dc..43fc61be5 100644 --- a/src/test/PoolTestBase.sol +++ b/src/test/PoolTestBase.sol @@ -20,7 +20,7 @@ abstract contract PoolTestBase is ILockCallback { if (withdrawTokens) { manager.take(currency, recipient, uint128(-amount)); } else { - manager.mint(currency, address(this), uint128(-amount)); + manager.mint(currency, recipient, uint128(-amount)); } } diff --git a/test/AccessLock.t.sol b/test/AccessLock.t.sol new file mode 100644 index 000000000..e96935ec4 --- /dev/null +++ b/test/AccessLock.t.sol @@ -0,0 +1,803 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {AccessLockHook, AccessLockHook2, AccessLockHook3} from "../src/test/AccessLockHook.sol"; +import {IPoolManager} from "../src/interfaces/IPoolManager.sol"; +import {PoolModifyPositionTest} from "../src/test/PoolModifyPositionTest.sol"; +import {PoolSwapTest} from "../src/test/PoolSwapTest.sol"; +import {PoolDonateTest} from "../src/test/PoolDonateTest.sol"; +import {Constants} from "./utils/Constants.sol"; +import {PoolKey} from "../src/types/PoolKey.sol"; +import {Deployers} from "./utils/Deployers.sol"; +import {Currency, CurrencyLibrary} from "../src/types/Currency.sol"; +import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; +import {Hooks} from "../src/libraries/Hooks.sol"; +import {IHooks} from "../src/interfaces/IHooks.sol"; +import {BalanceDelta} from "../src/types/BalanceDelta.sol"; +import {Pool} from "../src/libraries/Pool.sol"; +import {TickMath} from "../src/libraries/TickMath.sol"; +import {PoolIdLibrary} from "../src/types/PoolId.sol"; + +contract AccessLockTest is Test, Deployers { + using Pool for Pool.State; + using CurrencyLibrary for Currency; + using PoolIdLibrary for PoolKey; + + AccessLockHook accessLockHook; + AccessLockHook noAccessLockHook; + AccessLockHook2 accessLockHook2; + AccessLockHook3 accessLockHook3; + AccessLockHook accessLockHook4; + + function setUp() public { + // Initialize managers and routers. + deployFreshManagerAndRouters(); + (currency0, currency1) = deployMintAndApprove2Currencies(); + + // Create AccessLockHook. + address accessLockAddress = address( + uint160( + Hooks.ACCESS_LOCK_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG + | Hooks.BEFORE_DONATE_FLAG + ) + ); + deployCodeTo("AccessLockHook.sol:AccessLockHook", abi.encode(manager), accessLockAddress); + accessLockHook = AccessLockHook(accessLockAddress); + + (key,) = initPool( + currency0, currency1, IHooks(address(accessLockHook)), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES + ); + + // Create AccessLockHook2. + address accessLockAddress2 = address(uint160(Hooks.ACCESS_LOCK_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG)); + deployCodeTo("AccessLockHook.sol:AccessLockHook2", abi.encode(manager), accessLockAddress2); + accessLockHook2 = AccessLockHook2(accessLockAddress2); + + // Create AccessLockHook3. + address accessLockAddress3 = address( + (uint160(makeAddr("hook3")) << 10) >> 10 + | (uint160(Hooks.ACCESS_LOCK_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG)) + ); + deployCodeTo("AccessLockHook.sol:AccessLockHook3", abi.encode(manager), accessLockAddress3); + accessLockHook3 = AccessLockHook3(accessLockAddress3); + + // Create NoAccessLockHook. + address noAccessLockHookAddress = address(uint160(Hooks.BEFORE_MODIFY_POSITION_FLAG)); + deployCodeTo("AccessLockHook.sol:AccessLockHook", abi.encode(manager), noAccessLockHookAddress); + noAccessLockHook = AccessLockHook(noAccessLockHookAddress); + + // Create AccessLockHook with NoOp. + address accessLockHook4Address = address( + uint160( + Hooks.NO_OP_FLAG | Hooks.ACCESS_LOCK_FLAG | Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG + | Hooks.BEFORE_MODIFY_POSITION_FLAG | Hooks.BEFORE_DONATE_FLAG + ) + ); + deployCodeTo("AccessLockHook.sol:AccessLockHook", abi.encode(manager), accessLockHook4Address); + accessLockHook4 = AccessLockHook(accessLockHook4Address); + } + + function test_onlyByLocker_revertsForNoAccessLockPool() public { + (PoolKey memory keyWithoutAccessLockFlag,) = + initPool(currency0, currency1, IHooks(noAccessLockHook), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + + vm.expectRevert( + abi.encodeWithSelector( + IPoolManager.LockedBy.selector, address(modifyPositionRouter), address(noAccessLockHook) + ) + ); + modifyPositionRouter.modifyPosition( + keyWithoutAccessLockFlag, + IPoolManager.ModifyPositionParams({tickLower: 0, tickUpper: 60, liquidityDelta: 0}), + abi.encode(10, AccessLockHook.LockAction.Mint) // attempts a mint action that should revert + ); + } + /** + * + * + * The following test suite tests that appropriate hooks can call + * every function gated by the `onlyByLocker` modifier. + * We call these "LockActions". + * LockActions: + * - Mint + * - Take + * - Swap + * - ModifyPosition + * - Donate + * - Burn + * - Settle + * - Initialize + * Each of these calls is then tested from every callback after the + * currentHook gets set (beforeModifyPosition, beforeSwap, and beforeDonate). + * + */ + + /** + * + * BEFORE MODIFY POSITION TESTS + * + */ + function test_beforeModifyPosition_mint_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount < uint128(type(int128).max)); + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + BalanceDelta delta = modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.Mint) + ); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + + assertEq(manager.balanceOf(address(accessLockHook), currency1), amount); + } + + function test_beforeModifyPosition_take_succeedsWithAccessLock(uint128 amount) public { + // Add liquidity so there is something to take. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + // Can't take more than the manager has. + vm.assume(amount < key.currency1.balanceOf(address(manager))); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Hook only takes currency 1 rn. + BalanceDelta delta = modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(-60, 60, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.Take) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(address(accessLockHook)), amount); + } + + function test_beforeModifyPosition_swap_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10); // precision + + // Add liquidity so there is something to swap over. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Essentially "no-op"s the modifyPosition call and executes a swap before hand, applying the deltas from the swap to the locker. + modifyPositionRouter.modifyPosition( + key, IPoolManager.ModifyPositionParams(-120, 120, 0), abi.encode(amount, AccessLockHook.LockAction.Swap) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Balance decreases because we are swapping currency0 for currency1. + assertLt(balanceOfAfter0, balanceOfBefore0); + // Balance should be greater in currency1. + assertGt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeModifyPosition_modifyPosition_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < Pool.tickSpacingToMaxLiquidityPerTick(60)); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(-120, 120, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.ModifyPosition) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeModifyPosition_donate_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < uint128(type(int128).max)); // precision + // Add liquidity so there is a position to receive fees. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(-120, 120, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.Donate) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeModifyPosition_burn_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < uint128(type(int128).max)); // precision + // Add liquidity so there is a position to swap over. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + BalanceDelta delta = swapRouter.swap( + key, + IPoolManager.SwapParams(true, 10000, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 amount1 = uint256(uint128(-delta.amount1())); + // We have some balance in the manager. + assertEq(manager.balanceOf(address(this), currency1), amount1); + manager.transfer(address(key.hooks), currency1, amount1); + assertEq(manager.balanceOf(address(key.hooks), currency1), amount1); + + modifyPositionRouter.modifyPosition( + key, IPoolManager.ModifyPositionParams(-120, 120, 0), abi.encode(amount1, AccessLockHook.LockAction.Burn) + ); + + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfAfter1, balanceOfBefore1 + amount1); + } + + function test_beforeModifyPosition_settle_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < uint128(type(int128).max)); // precision + + // Add liquidity so there is something to take. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + // Can't take more than the manager has. + vm.assume(amount < key.currency1.balanceOf(address(manager))); + + // Assertions in the hook. Takes and then settles within the hook. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(-120, 120, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.Settle) + ); + } + + function test_beforeModifyPosition_initialize_succeedsWithAccessLock() public { + // The hook intitializes a new pool with the new key at Constants.SQRT_RATIO_1_2; + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(-120, 120, 1 * 10 ** 18), + abi.encode(0, AccessLockHook.LockAction.Initialize) + ); + + PoolKey memory newKey = PoolKey({ + currency0: key.currency0, + currency1: key.currency1, + fee: Constants.FEE_LOW, + tickSpacing: 60, + hooks: IHooks(address(0)) + }); + (Pool.Slot0 memory slot0,,,) = manager.pools(newKey.toId()); + + assertEq(slot0.sqrtPriceX96, Constants.SQRT_RATIO_1_2); + } + + /** + * + * BEFORE SWAP TESTS + * + */ + + function test_beforeSwap_mint_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount < uint128(type(int128).max)); + + // Add liquidity so there is something to swap against. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Small amount to swap (like NoOp). This way we can expect balances to just be from the hook applied delta. + BalanceDelta delta = swapRouter.swap( + key, + IPoolManager.SwapParams(true, 1, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(amount, AccessLockHook.LockAction.Mint) + ); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + + assertEq(manager.balanceOf(address(accessLockHook), currency1), amount); + } + + function test_beforeSwap_take_succeedsWithAccessLock(uint128 amount) public { + // Add liquidity so there is something to take. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + // Can't take more than the manager has. + vm.assume(amount < key.currency1.balanceOf(address(manager))); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Hook only takes currency 1 rn. + // Use small amount to NoOp. + BalanceDelta delta = swapRouter.swap( + key, + IPoolManager.SwapParams(true, 1, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(amount, AccessLockHook.LockAction.Take) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(address(accessLockHook)), amount); + } + + function test_beforeSwap_swap_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10); // precision + + // Add liquidity so there is something to swap over. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + swapRouter.swap( + key, + // Use small amounts so that the zeroForOne swap is larger + IPoolManager.SwapParams(false, 1, TickMath.MAX_SQRT_RATIO - 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(amount, AccessLockHook.LockAction.Swap) + ); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // The larger swap is zeroForOne + // Balance decreases because we are swapping currency0 for currency1. + assertLt(balanceOfAfter0, balanceOfBefore0); + // Balance should be greater in currency1. + assertGt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeSwap_modifyPosition_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < Pool.tickSpacingToMaxLiquidityPerTick(60)); + + // Add liquidity so there is something to swap over. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Make the swap amount small (like a NoOp). + swapRouter.swap( + key, + IPoolManager.SwapParams(true, 1, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(amount, AccessLockHook.LockAction.ModifyPosition) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeSwap_donate_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < uint128(type(int128).max)); // precision + // Add liquidity so there is a position to receive fees. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Make the swap amount small (like a NoOp). + swapRouter.swap( + key, + IPoolManager.SwapParams(true, 1, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(amount, AccessLockHook.LockAction.Donate) + ); + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + + /** + * + * BEFORE DONATE TESTS + * + */ + + function test_beforeDonate_mint_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount < uint128(type(int128).max)); + + // Add liquidity so there is something to donate to. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + BalanceDelta delta = + donateRouter.donate(key, 1 * 10 ** 18, 1 * 10 ** 18, abi.encode(amount, AccessLockHook.LockAction.Mint)); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the donateRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + + assertEq(manager.balanceOf(address(accessLockHook), currency1), amount); + } + + function test_beforeDonate_take_succeedsWithAccessLock(uint128 amount) public { + // Add liquidity so there is something to take. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + // Can't take more than the manager has. + vm.assume(amount < key.currency1.balanceOf(address(manager))); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Hook only takes currency 1 rn. + BalanceDelta delta = + donateRouter.donate(key, 1 * 10 ** 18, 1 * 10 ** 18, abi.encode(amount, AccessLockHook.LockAction.Take)); + // Take applies a positive delta in currency1. + // Donate applies a positive delta in currency0 and currency1. + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(address(accessLockHook)), amount); + } + + function test_beforeDonate_swap_succeedsWithAccessLock(uint128 amount) public { + // Add liquidity so there is something to swap over. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + // greater than 10 for precision, less than currency1 balance so that we still have liquidity we can donate to + vm.assume(amount != 0 && amount > 10 && amount < currency1.balanceOf(address(manager))); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Donate small amounts (NoOp) so we know the swap amount dominates. + donateRouter.donate(key, 1, 1, abi.encode(amount, AccessLockHook.LockAction.Swap)); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Balance of currency0 decreases bc we 1) donate and 2) swap zeroForOne. + assertLt(balanceOfAfter0, balanceOfBefore0); + // Since the donate amount is small, and we swapped zeroForOne, we expect balance of currency1 to increase. + assertGt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeDonate_modifyPosition_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < Pool.tickSpacingToMaxLiquidityPerTick(60)); + + // Add liquidity so there is something to donate to. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + donateRouter.donate( + key, 1 * 10 ** 18, 1 * 10 ** 18, abi.encode(amount, AccessLockHook.LockAction.ModifyPosition) + ); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies from adding liquidity AND donating. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + + function test_beforeDonate_donate_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount > 10 && amount < uint128(type(int128).max - 1)); // precision + + // Add liquidity so there is a position to receive fees. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + // Make the swap amount small (like a NoOp). + donateRouter.donate(key, 1 * 10 ** 18, 1 * 10 ** 18, abi.encode(amount, AccessLockHook.LockAction.Donate)); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + // Should have less balance in both currencies. + assertLt(balanceOfAfter0, balanceOfBefore0); + assertLt(balanceOfAfter1, balanceOfBefore1); + } + /** + * + * BEFORE INITIALIZE TESTS + * + */ + + function test_beforeInitialize_mint_succeedsWithAccessLock(uint128 amount) public { + vm.assume(amount != 0 && amount < uint128(type(int128).max)); + + PoolKey memory key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: Constants.FEE_MEDIUM, + tickSpacing: 60, + hooks: IHooks(address(accessLockHook4)) + }); + + initializeRouter.initialize(key1, SQRT_RATIO_1_1, abi.encode(amount, AccessLockHook.LockAction.Mint)); + + assertEq(manager.balanceOf(address(accessLockHook4), currency1), amount); + } + + function test_beforeInitialize_take_succeedsWithAccessLock(uint128 amount) public { + PoolKey memory key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: Constants.FEE_MEDIUM, + tickSpacing: 60, + hooks: IHooks(address(accessLockHook4)) + }); + + // Add liquidity to a different pool there is something to take. + modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams({tickLower: -120, tickUpper: 120, liquidityDelta: 100 * 10e18}), + ZERO_BYTES + ); + + // Can't take more than the manager has. + vm.assume(amount < key.currency1.balanceOf(address(manager))); + + initializeRouter.initialize(key1, SQRT_RATIO_1_1, abi.encode(amount, AccessLockHook.LockAction.Take)); + + assertEq(MockERC20(Currency.unwrap(currency1)).balanceOf(address(accessLockHook4)), amount); + } + + function test_beforeInitialize_swap_revertsOnPoolNotInitialized(uint128 amount) public { + vm.assume(amount != 0 && amount > 10); // precision + + PoolKey memory key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: Constants.FEE_MEDIUM, + tickSpacing: 60, + hooks: IHooks(address(accessLockHook4)) + }); + + vm.expectRevert(IPoolManager.PoolNotInitialized.selector); + initializeRouter.initialize(key1, SQRT_RATIO_1_1, abi.encode(amount, AccessLockHook.LockAction.Swap)); + } + + function test_beforeInitialize_modifyPosition_revertsOnPoolNotInitialized(uint128 amount) public { + vm.assume(amount != 0 && amount > 10); // precision + + PoolKey memory key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: Constants.FEE_MEDIUM, + tickSpacing: 60, + hooks: IHooks(address(accessLockHook4)) + }); + + vm.expectRevert(IPoolManager.PoolNotInitialized.selector); + initializeRouter.initialize(key1, SQRT_RATIO_1_1, abi.encode(amount, AccessLockHook.LockAction.ModifyPosition)); + } + + function test_beforeInitialize_donate_revertsOnPoolNotInitialized(uint128 amount) public { + vm.assume(amount != 0 && amount > 10); // precision + + PoolKey memory key1 = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: Constants.FEE_MEDIUM, + tickSpacing: 60, + hooks: IHooks(address(accessLockHook4)) + }); + + vm.expectRevert(IPoolManager.PoolNotInitialized.selector); + initializeRouter.initialize(key1, SQRT_RATIO_1_1, abi.encode(amount, AccessLockHook.LockAction.Donate)); + } + + /** + * + * EDGE CASE TESTS + * + */ + + function test_onlyByLocker_revertsWhenHookIsNotCurrentHook() public { + // Call first access lock hook. Should succeed. + uint256 amount = 100; + uint256 balanceOfBefore1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + uint256 balanceOfBefore0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + + BalanceDelta delta = modifyPositionRouter.modifyPosition( + key, + IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), + abi.encode(amount, AccessLockHook.LockAction.Mint) + ); + + uint256 balanceOfAfter0 = MockERC20(Currency.unwrap(currency0)).balanceOf(address(this)); + uint256 balanceOfAfter1 = MockERC20(Currency.unwrap(currency1)).balanceOf(address(this)); + + assertEq(balanceOfBefore0 - balanceOfAfter0, uint256(uint128(delta.amount0()))); + // The balance of our contract should be from the modifyPositionRouter (delta) AND the hook (amount). + assertEq(balanceOfBefore1 - balanceOfAfter1, uint256(amount + uint256(uint128(delta.amount1())))); + + assertEq(manager.balanceOf(address(accessLockHook), currency1), amount); + + assertEq(address(manager.getCurrentHook()), address(0)); + + (PoolKey memory keyAccessLockHook2,) = + initPool(currency0, currency1, IHooks(accessLockHook2), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + + // Delegates the beforeModifyPosition call to the hook in `key` which tries to mint on manager + // but reverts because hook in `key` is not the current hook. + vm.expectRevert( + abi.encodeWithSelector( + IPoolManager.LockedBy.selector, address(modifyPositionRouter), address(accessLockHook2) + ) + ); + delta = modifyPositionRouter.modifyPosition( + keyAccessLockHook2, IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), abi.encode(true, key) + ); + } + + function test_onlyByLocker_succeedsAfterHookMakesNestedCall() public { + (PoolKey memory keyWithNoHook,) = + initPool(currency0, currency1, IHooks(address(0)), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + + (PoolKey memory keyAccessLockHook2,) = + initPool(currency0, currency1, IHooks(accessLockHook2), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + + modifyPositionRouter.modifyPosition( + keyAccessLockHook2, IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), abi.encode(false, keyWithNoHook) + ); + assertEq(manager.balanceOf(address(accessLockHook2), currency1), 10); + } + + function test_onlyByLocker_revertsWhenThereIsNoOutsideLock() public { + modifyPositionRouter.modifyPosition(key, IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), ZERO_BYTES); + assertEq(address(manager.getCurrentHook()), address(0)); + + vm.expectRevert(abi.encodeWithSelector(IPoolManager.LockedBy.selector, address(0), address(0))); + vm.prank(address(key.hooks)); + manager.modifyPosition(key, IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), ZERO_BYTES); + } + + function test_getCurrentHook_isClearedAfterNestedLock() public { + // Create pool for AccessLockHook3. + (PoolKey memory keyAccessLockHook3,) = + initPool(currency0, currency1, IHooks(accessLockHook3), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + // Fund AccessLockHook3 with currency0. + MockERC20(Currency.unwrap(currency0)).transfer(address(accessLockHook3), 10); + assertEq(MockERC20(Currency.unwrap(currency0)).balanceOf(address(accessLockHook3)), 10); + + // Create pool to donate 10 of currency0 to inside of AccessLockHook3. This means AccessLockHook3 must acquire a new lock and settle. + // The currentHook addresses are checked inside this nested lock. + (PoolKey memory _key,) = + initPool(currency0, currency1, IHooks(address(0)), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + // Add liquidity so that the AccessLockHook3 can donate to something. + modifyPositionRouter.modifyPosition(_key, IPoolManager.ModifyPositionParams(-60, 60, 10 * 10 ** 18), ZERO_BYTES); + accessLockHook3.setKey(_key); + + // Asserts are in the AccessLockHook3. + modifyPositionRouter.modifyPosition( + keyAccessLockHook3, IPoolManager.ModifyPositionParams(0, 60, 1 * 10 ** 18), ZERO_BYTES + ); + } + + function test_getCurrentHook_isClearedAfterNoOpOnAllHooks() public { + (PoolKey memory noOpKey,) = + initPool(currency0, currency1, IHooks(accessLockHook4), Constants.FEE_MEDIUM, SQRT_RATIO_1_1, ZERO_BYTES); + + // Assertions for current hook address in AccessLockHook and respective routers. + // beforeModifyPosition noOp + modifyPositionRouter.modifyPosition( + noOpKey, + IPoolManager.ModifyPositionParams({tickLower: 0, tickUpper: 60, liquidityDelta: 0}), + abi.encode(0, AccessLockHook.LockAction.NoOp) + ); + + // beforeDonate noOp + donateRouter.donate(noOpKey, 1 * 10 ** 18, 1 * 10 ** 18, abi.encode(0, AccessLockHook.LockAction.NoOp)); + + // beforeSwap noOp + swapRouter.swap( + noOpKey, + IPoolManager.SwapParams(true, 1, TickMath.MIN_SQRT_RATIO + 1), + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}), + abi.encode(0, AccessLockHook.LockAction.NoOp) + ); + } +} diff --git a/test/CurrentHookAddress.t.sol b/test/CurrentHookAddress.t.sol new file mode 100644 index 000000000..199ce9af2 --- /dev/null +++ b/test/CurrentHookAddress.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Lockers} from "../src/libraries/Lockers.sol"; +import {IHooks} from "../src/interfaces/IHooks.sol"; + +contract CurrentHookAddressTest is Test { + function test_getCurrentHook() public { + assertEq(address(Lockers.getCurrentHook()), address(0)); + } + + function test_setCurrentHook() public { + Lockers.setCurrentHook(IHooks(address(1))); + assertEq(address(Lockers.getCurrentHook()), address(1)); + } + + function test_setCurrentHook_TwiceDoesNotSucceed() public { + (bool set) = Lockers.setCurrentHook(IHooks(address(1))); + assertTrue(set); + set = Lockers.setCurrentHook(IHooks(address(2))); + assertFalse(set); + assertEq(address(Lockers.getCurrentHook()), address(1)); + } + + function test_clearCurrentHook() public { + Lockers.setCurrentHook(IHooks(address(1))); + assertEq(address(Lockers.getCurrentHook()), address(1)); + Lockers.clearCurrentHook(); + assertEq(address(Lockers.getCurrentHook()), address(0)); + } + + function test_setCurrentHook_afterLock() public { + Lockers.push(address(this)); + Lockers.setCurrentHook(IHooks(address(1))); + assertEq(address(Lockers.getCurrentHook()), address(1)); + } + + function test_setCurrentHook_beforeAndAfterLock() public { + Lockers.push(address(this)); + Lockers.setCurrentHook(IHooks(address(2))); + assertEq(address(Lockers.getCurrentHook()), address(2)); + Lockers.push(address(1)); + assertEq(address(Lockers.getCurrentHook()), address(0)); + } +} diff --git a/test/Hooks.t.sol b/test/Hooks.t.sol index 54bdcf5f2..9bf1bb0c3 100644 --- a/test/Hooks.t.sol +++ b/test/Hooks.t.sol @@ -19,6 +19,9 @@ import {Deployers} from "./utils/Deployers.sol"; import {Fees} from "../src/Fees.sol"; import {PoolId, PoolIdLibrary} from "../src/types/PoolId.sol"; import {PoolKey} from "../src/types/PoolKey.sol"; +import {AccessLockHook} from "../src/test/AccessLockHook.sol"; +import {IERC20Minimal} from "../src/interfaces/external/IERC20Minimal.sol"; +import {BalanceDelta} from "../src/types/BalanceDelta.sol"; contract HooksTest is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -26,7 +29,13 @@ contract HooksTest is Test, Deployers, GasSnapshot { address payable ALL_HOOKS_ADDRESS = payable(0xfF00000000000000000000000000000000000000); MockHooks mockHooks; + // Update this value when you add a new hook flag. And then update all appropriate asserts. + uint256 hookPermissionCount = 10; + uint256 clearAllHookPermisssionsMask; + function setUp() public { + clearAllHookPermisssionsMask = uint256(~uint160(0) >> (hookPermissionCount)); + MockHooks impl = new MockHooks(); vm.etch(ALL_HOOKS_ADDRESS, address(impl).code); mockHooks = MockHooks(ALL_HOOKS_ADDRESS); @@ -34,7 +43,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { initializeManagerRoutersAndPoolsWithLiq(mockHooks); } - function testInitializeSucceedsWithHook() public { + function test_initialize_succeedsWithHook() public { initializeRouter.initialize(uninitializedKey, SQRT_RATIO_1_1, new bytes(123)); (uint160 sqrtPriceX96,,,) = manager.getSlot0(uninitializedKey.toId()); @@ -43,37 +52,37 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertEq(mockHooks.afterInitializeData(), new bytes(123)); } - function testBeforeInitializeInvalidReturn() public { + function test_beforeInitialize_invalidReturn() public { mockHooks.setReturnValue(mockHooks.beforeInitialize.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); initializeRouter.initialize(uninitializedKey, SQRT_RATIO_1_1, ZERO_BYTES); } - function testAfterInitializeInvalidReturn() public { + function test_afterInitialize_invalidReturn() public { mockHooks.setReturnValue(mockHooks.afterInitialize.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); initializeRouter.initialize(uninitializedKey, SQRT_RATIO_1_1, ZERO_BYTES); } - function testModifyPositionSucceedsWithHook() public { + function test_modifyPosition_succeedsWithHook() public { modifyPositionRouter.modifyPosition(key, LIQ_PARAMS, new bytes(111)); assertEq(mockHooks.beforeModifyPositionData(), new bytes(111)); assertEq(mockHooks.afterModifyPositionData(), new bytes(111)); } - function testBeforeModifyPositionInvalidReturn() public { + function test_beforeModifyPosition_invalidReturn() public { mockHooks.setReturnValue(mockHooks.beforeModifyPosition.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); modifyPositionRouter.modifyPosition(key, LIQ_PARAMS, ZERO_BYTES); } - function testAfterModifyPositionInvalidReturn() public { + function test_afterModifyPosition_invalidReturn() public { mockHooks.setReturnValue(mockHooks.afterModifyPosition.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); modifyPositionRouter.modifyPosition(key, LIQ_PARAMS, ZERO_BYTES); } - function testSwapSucceedsWithHook() public { + function test_swap_succeedsWithHook() public { IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: 100, sqrtPriceLimitX96: SQRT_RATIO_1_2}); @@ -85,7 +94,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertEq(mockHooks.afterSwapData(), new bytes(222)); } - function testBeforeSwapInvalidReturn() public { + function test_beforeSwap_invalidReturn() public { mockHooks.setReturnValue(mockHooks.beforeSwap.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); swapRouter.swap( @@ -96,7 +105,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { ); } - function testAfterSwapInvalidReturn() public { + function test_afterSwap_invalidReturn() public { mockHooks.setReturnValue(mockHooks.afterSwap.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); swapRouter.swap( @@ -107,19 +116,19 @@ contract HooksTest is Test, Deployers, GasSnapshot { ); } - function testDonateSucceedsWithHook() public { + function test_donate_succeedsWithHook() public { donateRouter.donate(key, 100, 200, new bytes(333)); assertEq(mockHooks.beforeDonateData(), new bytes(333)); assertEq(mockHooks.afterDonateData(), new bytes(333)); } - function testBeforeDonateInvalidReturn() public { + function test_beforeDonate_invalidReturn() public { mockHooks.setReturnValue(mockHooks.beforeDonate.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); donateRouter.donate(key, 100, 200, ZERO_BYTES); } - function testAfterDonateInvalidReturn() public { + function test_afterDonate_invalidReturn() public { mockHooks.setReturnValue(mockHooks.beforeDonate.selector, bytes4(0xdeadbeef)); vm.expectRevert(Hooks.InvalidHookResponse.selector); donateRouter.donate(key, 100, 200, ZERO_BYTES); @@ -127,12 +136,12 @@ contract HooksTest is Test, Deployers, GasSnapshot { // hook validation function testValidateHookAddressNoHooks(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(preAddr)); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -141,7 +150,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -153,15 +163,16 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeInitialize(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_INITIALIZE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, beforeModifyPosition: false, @@ -170,7 +181,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertTrue(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -182,15 +194,16 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressAfterInitialize(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.AFTER_INITIALIZE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: true, beforeModifyPosition: false, @@ -199,7 +212,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -211,14 +225,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeAndAfterInitialize(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: true, afterInitialize: true, beforeModifyPosition: false, @@ -227,7 +242,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertTrue(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -239,14 +255,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeModify(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_MODIFY_POSITION_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: true, @@ -255,7 +272,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -267,14 +285,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressAfterModify(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.AFTER_MODIFY_POSITION_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -283,7 +302,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -295,15 +315,16 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeAndAfterModify(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_MODIFY_POSITION_FLAG | Hooks.AFTER_MODIFY_POSITION_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: true, @@ -312,7 +333,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -324,15 +346,16 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeInitializeAfterModify(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_MODIFY_POSITION_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: true, afterInitialize: false, beforeModifyPosition: false, @@ -341,7 +364,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertTrue(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -353,14 +377,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeSwap(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_SWAP_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -369,7 +394,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -381,14 +407,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressAfterSwap(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.AFTER_SWAP_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -397,7 +424,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: true, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -409,14 +437,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeAndAfterSwap(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -425,7 +454,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: true, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -437,14 +467,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeDonate(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_DONATE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -453,7 +484,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: true, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -465,14 +497,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertTrue(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressAfterDonate(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.AFTER_DONATE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -481,7 +514,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: true, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -493,14 +527,15 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); assertTrue(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressBeforeAndAfterDonate(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.BEFORE_DONATE_FLAG | Hooks.AFTER_DONATE_FLAG))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -509,7 +544,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: true, afterDonate: true, - noOp: false + noOp: false, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -521,14 +557,46 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertTrue(Hooks.shouldCallBeforeDonate(hookAddr)); assertTrue(Hooks.shouldCallAfterDonate(hookAddr)); assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); + } + + function test_validateHookAddress_accessLock(uint160 addr) public { + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); + IHooks hookAddr = IHooks(address(uint160(preAddr | Hooks.ACCESS_LOCK_FLAG))); + Hooks.validateHookPermissions( + hookAddr, + Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeModifyPosition: false, + afterModifyPosition: false, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + noOp: false, + accessLock: true + }) + ); + assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); + assertFalse(Hooks.shouldCallAfterInitialize(hookAddr)); + assertFalse(Hooks.shouldCallBeforeModifyPosition(hookAddr)); + assertFalse(Hooks.shouldCallAfterModifyPosition(hookAddr)); + assertFalse(Hooks.shouldCallBeforeSwap(hookAddr)); + assertFalse(Hooks.shouldCallAfterSwap(hookAddr)); + assertFalse(Hooks.shouldCallBeforeDonate(hookAddr)); + assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); + assertFalse(Hooks.hasPermissionToNoOp(hookAddr)); + assertTrue(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressAllHooks(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); - IHooks hookAddr = IHooks(address(preAddr | (0xfF8 << 148))); - Hooks.validateHookAddress( + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); + uint160 allHookBitsFlipped = (~uint160(0)) << uint160((160 - hookPermissionCount)); + IHooks hookAddr = IHooks(address(uint160(preAddr) | allHookBitsFlipped)); + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: true, afterInitialize: true, beforeModifyPosition: true, @@ -537,7 +605,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: true, beforeDonate: true, afterDonate: true, - noOp: true + noOp: true, + accessLock: true }) ); assertTrue(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -549,10 +618,11 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertTrue(Hooks.shouldCallBeforeDonate(hookAddr)); assertTrue(Hooks.shouldCallAfterDonate(hookAddr)); assertTrue(Hooks.hasPermissionToNoOp(hookAddr)); + assertTrue(Hooks.hasPermissionToAccessLock(hookAddr)); } function testValidateHookAddressNoOp(uint160 addr) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); + uint160 preAddr = uint160(uint256(addr) & clearAllHookPermisssionsMask); IHooks hookAddr = IHooks( address( uint160( @@ -561,9 +631,9 @@ contract HooksTest is Test, Deployers, GasSnapshot { ) ) ); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: true, @@ -572,7 +642,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: true, afterDonate: false, - noOp: true + noOp: true, + accessLock: false }) ); assertFalse(Hooks.shouldCallBeforeInitialize(hookAddr)); @@ -584,17 +655,17 @@ contract HooksTest is Test, Deployers, GasSnapshot { assertTrue(Hooks.shouldCallBeforeDonate(hookAddr)); assertFalse(Hooks.shouldCallAfterDonate(hookAddr)); assertTrue(Hooks.hasPermissionToNoOp(hookAddr)); + assertFalse(Hooks.hasPermissionToAccessLock(hookAddr)); } - function testValidateHookAddressFailsAllHooks(uint160 addr, uint16 mask) public { - uint160 preAddr = addr & uint160(0x007ffffFfffffffffFffffFFfFFFFFFffFFfFFff); // clear the first 9 bits - mask = mask & 0xff80; // the last 7 bits are all 0, we just want a 9 bit mask - vm.assume(mask != 0xff80); // we want any combination except all hooks - IHooks hookAddr = IHooks(address(preAddr | (uint160(mask) << 144))); + function testValidateHookAddressFailsAllHooks(uint152 addr, uint8 mask) public { + uint160 preAddr = uint160(uint256(addr)); + vm.assume(mask != 0xff8); + IHooks hookAddr = IHooks(address(uint160(preAddr) | (uint160(mask) << 151))); vm.expectRevert(abi.encodeWithSelector(Hooks.HookAddressNotValid.selector, (address(hookAddr)))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: true, afterInitialize: true, beforeModifyPosition: true, @@ -603,7 +674,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: true, beforeDonate: true, afterDonate: true, - noOp: true + noOp: true, + accessLock: true }) ); } @@ -614,9 +686,9 @@ contract HooksTest is Test, Deployers, GasSnapshot { vm.assume(mask != 0); // we want any combination except no hooks IHooks hookAddr = IHooks(address(preAddr | (uint160(mask) << 144))); vm.expectRevert(abi.encodeWithSelector(Hooks.HookAddressNotValid.selector, (address(hookAddr)))); - Hooks.validateHookAddress( + Hooks.validateHookPermissions( hookAddr, - Hooks.Calls({ + Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeModifyPosition: false, @@ -625,7 +697,8 @@ contract HooksTest is Test, Deployers, GasSnapshot { afterSwap: false, beforeDonate: false, afterDonate: false, - noOp: false + noOp: false, + accessLock: false }) ); } @@ -665,7 +738,7 @@ contract HooksTest is Test, Deployers, GasSnapshot { function testInvalidIfNoFlags() public { assertFalse(Hooks.isValidHookAddress(IHooks(0x0000000000000000000000000000000000000001), 3000)); - assertFalse(Hooks.isValidHookAddress(IHooks(0x0040000000000000000000000000000000000001), 3000)); - assertFalse(Hooks.isValidHookAddress(IHooks(0x007840A85d5aF5BF1D1762f925bdADDC4201F984), 3000)); + assertFalse(Hooks.isValidHookAddress(IHooks(0x0020000000000000000000000000000000000001), 3000)); + assertFalse(Hooks.isValidHookAddress(IHooks(0x003840a85d5Af5Bf1d1762F925BDADDc4201f984), 3000)); } } diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 8baa2ec4a..299dd8001 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -407,12 +407,12 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true}); vm.expectEmit(true, true, true, false); - emit Mint(address(swapRouter), currency1, 98); + emit Mint(address(this), currency1, 98); snapStart("swap mint output as claim"); swapRouter.swap(key, params, testSettings, ZERO_BYTES); snapEnd(); - uint256 claimsBalance = manager.balanceOf(address(swapRouter), currency1); + uint256 claimsBalance = manager.balanceOf(address(this), currency1); assertEq(claimsBalance, 98); } @@ -424,16 +424,17 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings({withdrawTokens: false, settleUsingTransfer: true}); vm.expectEmit(true, true, true, false); - emit Mint(address(swapRouter), currency1, 98); + emit Mint(address(this), currency1, 98); swapRouter.swap(key, params, testSettings, ZERO_BYTES); - uint256 claimsBalance = manager.balanceOf(address(swapRouter), currency1); + uint256 claimsBalance = manager.balanceOf(address(this), currency1); assertEq(claimsBalance, 98); // swap from currency1 to currency0 again, using Claims as input tokens params = IPoolManager.SwapParams({zeroForOne: false, amountSpecified: -25, sqrtPriceLimitX96: SQRT_RATIO_4_1}); testSettings = PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: false}); + manager.transfer(address(swapRouter), currency1, claimsBalance); vm.expectEmit(true, true, true, false); emit Burn(address(swapRouter), currency1, 27); diff --git a/test/utils/Constants.sol b/test/utils/Constants.sol index 42694631c..354a2d394 100644 --- a/test/utils/Constants.sol +++ b/test/utils/Constants.sol @@ -15,4 +15,8 @@ library Constants { uint256 constant POOL_SLOT = 10; uint256 constant TICKS_OFFSET = 4; + + uint24 constant FEE_LOW = 500; + uint24 constant FEE_MEDIUM = 3000; + uint24 constant FEE_HIGH = 10000; } diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index 8debd44f2..09af05131 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -65,14 +65,19 @@ contract Deployers { } function deployMintAndApprove2Currencies() internal returns (Currency, Currency) { - MockERC20[] memory tokens = deployTokens(2, 1000 ether); + MockERC20[] memory tokens = deployTokens(2, 2 ** 255); - address[4] memory toApprove = - [address(swapRouter), address(modifyPositionRouter), address(donateRouter), address(takeRouter)]; + address[5] memory toApprove = [ + address(swapRouter), + address(modifyPositionRouter), + address(donateRouter), + address(takeRouter), + address(initializeRouter) + ]; for (uint256 i = 0; i < toApprove.length; i++) { - tokens[0].approve(toApprove[i], 1000 ether); - tokens[1].approve(toApprove[i], 1000 ether); + tokens[0].approve(toApprove[i], Constants.MAX_UINT256); + tokens[1].approve(toApprove[i], Constants.MAX_UINT256); } return SortTokens.sort(tokens[0], tokens[1]);