diff --git a/.forge-snapshots/FullRangeAddInitialLiquidity.snap b/.forge-snapshots/FullRangeAddInitialLiquidity.snap index cd1e3c37..b9d81858 100644 --- a/.forge-snapshots/FullRangeAddInitialLiquidity.snap +++ b/.forge-snapshots/FullRangeAddInitialLiquidity.snap @@ -1 +1 @@ -311073 \ No newline at end of file +311137 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeAddLiquidity.snap b/.forge-snapshots/FullRangeAddLiquidity.snap index b3de2b4e..c3edfa69 100644 --- a/.forge-snapshots/FullRangeAddLiquidity.snap +++ b/.forge-snapshots/FullRangeAddLiquidity.snap @@ -1 +1 @@ -122882 \ No newline at end of file +122946 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeFirstSwap.snap b/.forge-snapshots/FullRangeFirstSwap.snap index 54d0d097..b9e04365 100644 --- a/.forge-snapshots/FullRangeFirstSwap.snap +++ b/.forge-snapshots/FullRangeFirstSwap.snap @@ -1 +1 @@ -80283 \ No newline at end of file +80287 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeInitialize.snap b/.forge-snapshots/FullRangeInitialize.snap index f81651f8..7a0170eb 100644 --- a/.forge-snapshots/FullRangeInitialize.snap +++ b/.forge-snapshots/FullRangeInitialize.snap @@ -1 +1 @@ -1015169 \ No newline at end of file +1015181 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidity.snap b/.forge-snapshots/FullRangeRemoveLiquidity.snap index 265c1dec..4444368b 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidity.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidity.snap @@ -1 +1 @@ -110476 \ No newline at end of file +110544 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap index dcb62527..1bc2d893 100644 --- a/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap +++ b/.forge-snapshots/FullRangeRemoveLiquidityAndRebalance.snap @@ -1 +1 @@ -239954 \ No newline at end of file +240022 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSecondSwap.snap b/.forge-snapshots/FullRangeSecondSwap.snap index 1bf183ef..c1cac22b 100644 --- a/.forge-snapshots/FullRangeSecondSwap.snap +++ b/.forge-snapshots/FullRangeSecondSwap.snap @@ -1 +1 @@ -45993 \ No newline at end of file +45997 \ No newline at end of file diff --git a/.forge-snapshots/FullRangeSwap.snap b/.forge-snapshots/FullRangeSwap.snap index 5630ac05..97d86500 100644 --- a/.forge-snapshots/FullRangeSwap.snap +++ b/.forge-snapshots/FullRangeSwap.snap @@ -1 +1 @@ -79414 \ No newline at end of file +79418 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index b2759d7f..03924f26 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122355 \ No newline at end of file +122359 \ No newline at end of file diff --git a/README.md b/README.md index b3355a10..b5be65fa 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ contract CoolHook is BaseHook { address, IPoolManager.PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { // hook logic return BaseHook.beforeAddLiquidity.selector; } diff --git a/contracts/BaseHook.sol b/contracts/BaseHook.sol index eb75502c..7a31a8d9 100644 --- a/contracts/BaseHook.sol +++ b/contracts/BaseHook.sol @@ -6,29 +6,20 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {SafeCallback} from "./base/SafeCallback.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol"; -abstract contract BaseHook is IHooks { - error NotPoolManager(); +abstract contract BaseHook is IHooks, SafeCallback { error NotSelf(); error InvalidPool(); error LockFailure(); error HookNotImplemented(); - /// @notice The address of the pool manager - IPoolManager public immutable poolManager; - - constructor(IPoolManager _poolManager) { - poolManager = _poolManager; + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) { validateHookAddress(this); } - /// @dev Only the pool manager may call this function - modifier poolManagerOnly() { - if (msg.sender != address(poolManager)) revert NotPoolManager(); - _; - } - /// @dev Only this address may call this function modifier selfOnly() { if (msg.sender != address(this)) revert NotSelf(); @@ -50,7 +41,7 @@ abstract contract BaseHook is IHooks { Hooks.validateHookPermissions(_this, getHookPermissions()); } - function unlockCallback(bytes calldata data) external virtual poolManagerOnly returns (bytes memory) { + function _unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { (bool success, bytes memory returnData) = address(this).call(data); if (success) return returnData; if (returnData.length == 0) revert LockFailure(); diff --git a/contracts/SimpleBatchCall.sol b/contracts/SimpleBatchCall.sol new file mode 100644 index 00000000..e203becc --- /dev/null +++ b/contracts/SimpleBatchCall.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {LockAndBatchCall} from "./base/LockAndBatchCall.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./base/ImmutableState.sol"; +import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol"; +import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol"; + +/// @title SimpleBatchCall +/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. +contract SimpleBatchCall is LockAndBatchCall { + using CurrencyLibrary for Currency; + using TransientStateLibrary for IPoolManager; + using CurrencySettler for Currency; + + constructor(IPoolManager _poolManager) ImmutableState(_poolManager) {} + + struct SettleConfig { + bool takeClaims; + bool settleUsingBurn; // If true, sends the underlying ERC20s. + } + + /// @notice We naively settle all currencies that are touched by the batch call. This data is passed in intially to `execute`. + function _settle(address sender, bytes memory data) internal override returns (bytes memory settleData) { + if (data.length != 0) { + (Currency[] memory currenciesTouched, SettleConfig memory config) = + abi.decode(data, (Currency[], SettleConfig)); + + for (uint256 i = 0; i < currenciesTouched.length; i++) { + Currency currency = currenciesTouched[i]; + int256 delta = poolManager.currencyDelta(address(this), currenciesTouched[i]); + + if (delta < 0) { + currency.settle(poolManager, sender, uint256(-delta), config.settleUsingBurn); + } + if (delta > 0) { + currency.take(poolManager, address(this), uint256(delta), config.takeClaims); + } + } + } + } + + function _handleAfterExecute(bytes memory, /*callReturnData*/ bytes memory /*settleReturnData*/ ) + internal + pure + override + { + return; + } +} diff --git a/contracts/base/CallsWithLock.sol b/contracts/base/CallsWithLock.sol new file mode 100644 index 00000000..113d1ebd --- /dev/null +++ b/contracts/base/CallsWithLock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {ImmutableState} from "./ImmutableState.sol"; +import {ICallsWithLock} from "../interfaces/ICallsWithLock.sol"; +import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol"; + +/// @title CallsWithLock +/// @notice Handles all the calls to the pool manager contract. Assumes the integrating contract has already acquired a lock. +abstract contract CallsWithLock is ICallsWithLock, ImmutableState { + error NotSelf(); + + modifier onlyBySelf() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.initialize(key, sqrtPriceX96, hookData)); + } + + function modifyPositionWithLock( + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external onlyBySelf returns (bytes memory) { + (BalanceDelta delta, BalanceDelta feeDelta) = poolManager.modifyLiquidity(key, params, hookData); + return abi.encode(delta, feeDelta); + } + + function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.swap(key, params, hookData)); + } + + function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + onlyBySelf + returns (bytes memory) + { + return abi.encode(poolManager.donate(key, amount0, amount1, hookData)); + } +} diff --git a/contracts/base/ImmutableState.sol b/contracts/base/ImmutableState.sol new file mode 100644 index 00000000..7208c302 --- /dev/null +++ b/contracts/base/ImmutableState.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +contract ImmutableState { + IPoolManager public immutable poolManager; + + constructor(IPoolManager _manager) { + poolManager = _manager; + } +} diff --git a/contracts/base/LockAndBatchCall.sol b/contracts/base/LockAndBatchCall.sol new file mode 100644 index 00000000..5a312b5f --- /dev/null +++ b/contracts/base/LockAndBatchCall.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {SafeCallback} from "./SafeCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {CallsWithLock} from "./CallsWithLock.sol"; + +abstract contract LockAndBatchCall is CallsWithLock, SafeCallback { + error CallFail(bytes reason); + + function _settle(address sender, bytes memory data) internal virtual returns (bytes memory settleData); + function _handleAfterExecute(bytes memory callReturnData, bytes memory settleReturnData) internal virtual; + + /// @param executeData The function selectors and calldata for any of the function selectors in ICallsWithLock encoded as an array of bytes. + function execute(bytes memory executeData, bytes memory settleData) external { + (bytes memory lockReturnData) = + poolManager.unlock(abi.encode(executeData, abi.encode(msg.sender, settleData))); + (bytes memory executeReturnData, bytes memory settleReturnData) = abi.decode(lockReturnData, (bytes, bytes)); + _handleAfterExecute(executeReturnData, settleReturnData); + } + + /// @param data This data is passed from the top-level execute function to the internal _executeWithLockCalls and _settle function. It is decoded as two separate dynamic bytes parameters. + /// @dev _unlockCallback is responsible for executing the internal calls under the lock and settling open deltas left on the pool + function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (bytes memory executeData, bytes memory settleDataWithSender) = abi.decode(data, (bytes, bytes)); + (address sender, bytes memory settleData) = abi.decode(settleDataWithSender, (address, bytes)); + return abi.encode(_executeWithLockCalls(executeData), _settle(sender, settleData)); + } + + function _executeWithLockCalls(bytes memory data) internal returns (bytes memory) { + bytes[] memory calls = abi.decode(data, (bytes[])); + bytes[] memory callsReturnData = new bytes[](calls.length); + + for (uint256 i = 0; i < calls.length; i++) { + (bool success, bytes memory returnData) = address(this).call(calls[i]); + if (!success) revert(string(returnData)); + callsReturnData[i] = returnData; + } + return abi.encode(callsReturnData); + } +} diff --git a/contracts/base/SafeCallback.sol b/contracts/base/SafeCallback.sol new file mode 100644 index 00000000..3eb693dd --- /dev/null +++ b/contracts/base/SafeCallback.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {ImmutableState} from "./ImmutableState.sol"; + +abstract contract SafeCallback is ImmutableState, IUnlockCallback { + error NotManager(); + + modifier onlyByManager() { + if (msg.sender != address(poolManager)) revert NotManager(); + _; + } + + /// @dev There is no way to force the onlyByManager modifier but for this callback to be safe, it MUST check that the msg.sender is the pool manager. + function unlockCallback(bytes calldata data) external onlyByManager returns (bytes memory) { + return _unlockCallback(data); + } + + function _unlockCallback(bytes calldata data) internal virtual returns (bytes memory); +} diff --git a/contracts/hooks/examples/FullRange.sol b/contracts/hooks/examples/FullRange.sol index 194be803..49fe773f 100644 --- a/contracts/hooks/examples/FullRange.sol +++ b/contracts/hooks/examples/FullRange.sol @@ -26,7 +26,7 @@ import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/src/type import "../../libraries/LiquidityAmounts.sol"; -contract FullRange is BaseHook, IUnlockCallback { +contract FullRange is BaseHook { using CurrencyLibrary for Currency; using CurrencySettler for Currency; using PoolIdLibrary for PoolKey; @@ -295,10 +295,9 @@ contract FullRange is BaseHook, IUnlockCallback { pool.hasAccruedFees = false; } - function unlockCallback(bytes calldata rawData) - external - override(IUnlockCallback, BaseHook) - poolManagerOnly + function _unlockCallback(bytes calldata rawData) + internal + override returns (bytes memory) { CallbackData memory data = abi.decode(rawData, (CallbackData)); diff --git a/contracts/hooks/examples/GeomeanOracle.sol b/contracts/hooks/examples/GeomeanOracle.sol index ec8301a5..137d4207 100644 --- a/contracts/hooks/examples/GeomeanOracle.sol +++ b/contracts/hooks/examples/GeomeanOracle.sol @@ -86,7 +86,7 @@ contract GeomeanOracle is BaseHook { external view override - poolManagerOnly + onlyByManager returns (bytes4) { // This is to limit the fragmentation of pools using this oracle hook. In other words, @@ -99,7 +99,7 @@ contract GeomeanOracle is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { PoolId id = key.toId(); @@ -124,7 +124,8 @@ contract GeomeanOracle is BaseHook { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata params, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { + if (params.liquidityDelta < 0) revert OraclePoolMustLockLiquidity(); int24 maxTickSpacing = poolManager.MAX_TICK_SPACING(); if ( params.tickLower != TickMath.minUsableTick(maxTickSpacing) @@ -139,14 +140,14 @@ contract GeomeanOracle is BaseHook { PoolKey calldata, IPoolManager.ModifyLiquidityParams calldata, bytes calldata - ) external view override poolManagerOnly returns (bytes4) { + ) external view override onlyByManager returns (bytes4) { revert OraclePoolMustLockLiquidity(); } function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4, BeforeSwapDelta, uint24) { _updatePool(key); diff --git a/contracts/hooks/examples/LimitOrder.sol b/contracts/hooks/examples/LimitOrder.sol index 9ee7a33c..3d26f740 100644 --- a/contracts/hooks/examples/LimitOrder.sol +++ b/contracts/hooks/examples/LimitOrder.sol @@ -129,7 +129,7 @@ contract LimitOrder is BaseHook { function afterInitialize(address, PoolKey calldata key, uint160, int24 tick, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4) { setTickLowerLast(key.toId(), getTickLower(tick, key.tickSpacing)); @@ -142,7 +142,7 @@ contract LimitOrder is BaseHook { IPoolManager.SwapParams calldata params, BalanceDelta, bytes calldata - ) external override poolManagerOnly returns (bytes4, int128) { + ) external override onlyByManager returns (bytes4, int128) { (int24 tickLower, int24 lower, int24 upper) = _getCrossedTicks(key.toId(), key.tickSpacing); if (lower > upper) return (LimitOrder.afterSwap.selector, 0); @@ -197,7 +197,7 @@ contract LimitOrder is BaseHook { function _unlockCallbackFill(PoolKey calldata key, int24 tickLower, int256 liquidityDelta) private - poolManagerOnly + onlyByManager returns (uint128 amount0, uint128 amount1) { (BalanceDelta delta,) = poolManager.modifyLiquidity( diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index 5704d765..c619e900 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -88,7 +88,7 @@ contract TWAMM is BaseHook, ITWAMM { external virtual override - poolManagerOnly + onlyByManager returns (bytes4) { // one-time initialization enforced in PoolManager @@ -101,7 +101,7 @@ contract TWAMM is BaseHook, ITWAMM { PoolKey calldata key, IPoolManager.ModifyLiquidityParams calldata, bytes calldata - ) external override poolManagerOnly returns (bytes4) { + ) external override onlyByManager returns (bytes4) { executeTWAMMOrders(key); return BaseHook.beforeAddLiquidity.selector; } @@ -109,7 +109,7 @@ contract TWAMM is BaseHook, ITWAMM { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - poolManagerOnly + onlyByManager returns (bytes4, BeforeSwapDelta, uint24) { executeTWAMMOrders(key); @@ -309,7 +309,7 @@ contract TWAMM is BaseHook, ITWAMM { IERC20Minimal(Currency.unwrap(token)).safeTransfer(to, amountTransferred); } - function unlockCallback(bytes calldata rawData) external override poolManagerOnly returns (bytes memory) { + function _unlockCallback(bytes calldata rawData) internal override returns (bytes memory) { (PoolKey memory key, IPoolManager.SwapParams memory swapParams) = abi.decode(rawData, (PoolKey, IPoolManager.SwapParams)); diff --git a/contracts/interfaces/ICallsWithLock.sol b/contracts/interfaces/ICallsWithLock.sol new file mode 100644 index 00000000..26017356 --- /dev/null +++ b/contracts/interfaces/ICallsWithLock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; + +interface ICallsWithLock { + function initializeWithLock(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + returns (bytes memory); + + function modifyPositionWithLock( + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata params, + bytes calldata hookData + ) external returns (bytes memory); + + function swapWithLock(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + external + returns (bytes memory); + + function donateWithLock(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + returns (bytes memory); +} diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol index 17f5aecb..29b1093f 100644 --- a/test/LimitOrder.t.sol +++ b/test/LimitOrder.t.sol @@ -21,7 +21,7 @@ contract TestLimitOrder is Test, Deployers { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; - uint160 constant SQRT_RATIO_10_1 = 250541448375047931186413801569; + uint160 constant SQRT_PRICE_10_1 = 250541448375047931186413801569; HookEnabledSwapRouter router; TestERC20 token0; @@ -65,7 +65,7 @@ contract TestLimitOrder is Test, Deployers { function testGetTickLowerLastWithDifferentPrice() public { PoolKey memory differentKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 61, limitOrder); - manager.initialize(differentKey, SQRT_RATIO_10_1, ZERO_BYTES); + manager.initialize(differentKey, SQRT_PRICE_10_1, ZERO_BYTES); assertEq(limitOrder.getTickLowerLast(differentKey.toId()), 22997); } diff --git a/test/Quoter.t.sol b/test/Quoter.t.sol index 0767cadd..f434fd19 100644 --- a/test/Quoter.t.sol +++ b/test/Quoter.t.sol @@ -31,8 +31,8 @@ contract QuoterTest is Test, Deployers { // Max tick for full range with tick spacing of 60 int24 internal constant MAX_TICK = -MIN_TICK; - uint160 internal constant SQRT_RATIO_100_102 = 78447570448055484695608110440; - uint160 internal constant SQRT_RATIO_102_100 = 80016521857016594389520272648; + uint160 internal constant SQRT_PRICE_100_102 = 78447570448055484695608110440; + uint160 internal constant SQRT_PRICE_102_100 = 80016521857016594389520272648; uint256 internal constant CONTROLLER_GAS_LIMIT = 500000; @@ -327,13 +327,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: true, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_100_102, + sqrtPriceLimitX96: SQRT_PRICE_100_102, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[0], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_100_102); + assertEq(sqrtPriceX96After, SQRT_PRICE_100_102); assertEq(initializedTicksLoaded, 0); } @@ -345,13 +345,13 @@ contract QuoterTest is Test, Deployers { zeroForOne: false, recipient: address(this), exactAmount: type(uint128).max, - sqrtPriceLimitX96: SQRT_RATIO_102_100, + sqrtPriceLimitX96: SQRT_PRICE_102_100, hookData: ZERO_BYTES }) ); assertEq(deltaAmounts[1], 9981); - assertEq(sqrtPriceX96After, SQRT_RATIO_102_100); + assertEq(sqrtPriceX96After, SQRT_PRICE_102_100); assertEq(initializedTicksLoaded, 0); } diff --git a/test/SimpleBatchCallTest.t.sol b/test/SimpleBatchCallTest.t.sol new file mode 100644 index 00000000..b504e376 --- /dev/null +++ b/test/SimpleBatchCallTest.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {SimpleBatchCall} from "../contracts/SimpleBatchCall.sol"; +import {ICallsWithLock} from "../contracts/interfaces/ICallsWithLock.sol"; + +import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; +import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; +import {Pool} from "@uniswap/v4-core/src/libraries/Pool.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol"; + +/// @title SimpleBatchCall +/// @notice Implements a naive settle function to perform any arbitrary batch call under one lock to modifyPosition, donate, intitialize, or swap. +contract SimpleBatchCallTest is Test, Deployers { + using PoolIdLibrary for PoolKey; + using StateLibrary for IPoolManager; + + SimpleBatchCall batchCall; + + function setUp() public { + Deployers.deployFreshManagerAndRouters(); + Deployers.deployMintAndApprove2Currencies(); + key = + PoolKey({currency0: currency0, currency1: currency1, fee: 3000, tickSpacing: 60, hooks: IHooks(address(0))}); + + batchCall = new SimpleBatchCall(manager); + ERC20(Currency.unwrap(currency0)).approve(address(batchCall), 2 ** 255); + ERC20(Currency.unwrap(currency1)).approve(address(batchCall), 2 ** 255); + } + + function test_initialize() public { + bytes[] memory calls = new bytes[](1); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + bytes memory settleData = + abi.encode(SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false})); + batchCall.execute(abi.encode(calls), ZERO_BYTES); + + (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); + } + + function test_initialize_modifyPosition() public { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeWithSelector(ICallsWithLock.initializeWithLock.selector, key, SQRT_PRICE_1_1, ZERO_BYTES); + calls[1] = abi.encodeWithSelector( + ICallsWithLock.modifyPositionWithLock.selector, + key, + IPoolManager.ModifyLiquidityParams({tickLower: -60, tickUpper: 60, liquidityDelta: 10 * 10 ** 18, salt: 0}), + ZERO_BYTES + ); + Currency[] memory currenciesTouched = new Currency[](2); + currenciesTouched[0] = currency0; + currenciesTouched[1] = currency1; + bytes memory settleData = abi.encode( + currenciesTouched, SimpleBatchCall.SettleConfig({takeClaims: false, settleUsingBurn: false}) + ); + uint256 balance0 = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); + uint256 balance1 = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); + batchCall.execute(abi.encode(calls), settleData); + uint256 balance0After = ERC20(Currency.unwrap(currency0)).balanceOf(address(manager)); + uint256 balance1After = ERC20(Currency.unwrap(currency1)).balanceOf(address(manager)); + + (uint160 sqrtPriceX96,,,) = manager.getSlot0(key.toId()); + + assertGt(balance0After, balance0); + assertGt(balance1After, balance1); + assertEq(sqrtPriceX96, SQRT_PRICE_1_1); + } +}