diff --git a/.forge-snapshots/NoDelegateCallOverhead.snap b/.forge-snapshots/NoDelegateCall.snap similarity index 100% rename from .forge-snapshots/NoDelegateCallOverhead.snap rename to .forge-snapshots/NoDelegateCall.snap diff --git a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap index 38ef83141..43e6c3379 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactInPartial.snap @@ -1 +1 @@ -3038 \ No newline at end of file +3016 \ No newline at end of file diff --git a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap index e4da9ac19..72cd61fe0 100644 --- a/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap +++ b/.forge-snapshots/SwapMath_oneForZero_exactOutPartial.snap @@ -1 +1 @@ -3278 \ No newline at end of file +3256 \ No newline at end of file diff --git a/.forge-snapshots/add liquidity to already existing position with salt.snap b/.forge-snapshots/add liquidity to already existing position with salt.snap index c499532e1..02518aa3d 100644 --- a/.forge-snapshots/add liquidity to already existing position with salt.snap +++ b/.forge-snapshots/add liquidity to already existing position with salt.snap @@ -1 +1 @@ -151023 \ No newline at end of file +150975 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity CA fee.snap b/.forge-snapshots/addLiquidity CA fee.snap index 2f1d86196..4bbe5336b 100644 --- a/.forge-snapshots/addLiquidity CA fee.snap +++ b/.forge-snapshots/addLiquidity CA fee.snap @@ -1 +1 @@ -329389 \ No newline at end of file +329315 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with empty hook.snap b/.forge-snapshots/addLiquidity with empty hook.snap index 2c8de072a..f2bb6a577 100644 --- a/.forge-snapshots/addLiquidity with empty hook.snap +++ b/.forge-snapshots/addLiquidity with empty hook.snap @@ -1 +1 @@ -284024 \ No newline at end of file +283956 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity with native token.snap b/.forge-snapshots/addLiquidity with native token.snap index 98a8898a9..99411cb4c 100644 --- a/.forge-snapshots/addLiquidity with native token.snap +++ b/.forge-snapshots/addLiquidity with native token.snap @@ -1 +1 @@ -141199 \ No newline at end of file +141108 \ No newline at end of file diff --git a/.forge-snapshots/addLiquidity.snap b/.forge-snapshots/addLiquidity.snap index c5e38ac35..284523239 100644 --- a/.forge-snapshots/addLiquidity.snap +++ b/.forge-snapshots/addLiquidity.snap @@ -1 +1 @@ -150999 \ No newline at end of file +150951 \ No newline at end of file diff --git a/.forge-snapshots/create new liquidity to a position with salt.snap b/.forge-snapshots/create new liquidity to a position with salt.snap index 6bd8b66e2..0bb6dbe67 100644 --- a/.forge-snapshots/create new liquidity to a position with salt.snap +++ b/.forge-snapshots/create new liquidity to a position with salt.snap @@ -1 +1 @@ -299551 \ No newline at end of file +299503 \ 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 835237096..efe7a735d 100644 --- a/.forge-snapshots/donate gas with 1 token.snap +++ b/.forge-snapshots/donate gas with 1 token.snap @@ -1 +1 @@ -108887 \ No newline at end of file +108687 \ 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 f89390d96..6a6f4c603 100644 --- a/.forge-snapshots/donate gas with 2 tokens.snap +++ b/.forge-snapshots/donate gas with 2 tokens.snap @@ -1 +1 @@ -149382 \ No newline at end of file +149222 \ No newline at end of file diff --git a/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap b/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap index 7dfce3516..23c5f49dc 100644 --- a/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap +++ b/.forge-snapshots/getNextSqrtPriceFromInput_zeroForOneEqualsFalseGas.snap @@ -1 +1 @@ -594 \ No newline at end of file +572 \ No newline at end of file diff --git a/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap b/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap index f8f450742..c5befbc75 100644 --- a/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap +++ b/.forge-snapshots/getNextSqrtPriceFromOutput_zeroForOneEqualsFalseGas.snap @@ -1 +1 @@ -878 \ No newline at end of file +856 \ No newline at end of file diff --git a/.forge-snapshots/poolManager bytecode size.snap b/.forge-snapshots/poolManager bytecode size.snap index d000b5ae5..1acfffea3 100644 --- a/.forge-snapshots/poolManager bytecode size.snap +++ b/.forge-snapshots/poolManager bytecode size.snap @@ -1 +1 @@ -23386 \ No newline at end of file +23658 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity CA fee.snap b/.forge-snapshots/removeLiquidity CA fee.snap index e26a1269f..dc19e254e 100644 --- a/.forge-snapshots/removeLiquidity CA fee.snap +++ b/.forge-snapshots/removeLiquidity CA fee.snap @@ -1 +1 @@ -184962 \ No newline at end of file +184802 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with empty hook.snap b/.forge-snapshots/removeLiquidity with empty hook.snap index ffcbaf2b7..488660be0 100644 --- a/.forge-snapshots/removeLiquidity with empty hook.snap +++ b/.forge-snapshots/removeLiquidity with empty hook.snap @@ -1 +1 @@ -120980 \ No newline at end of file +120846 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity with native token.snap b/.forge-snapshots/removeLiquidity with native token.snap index ae1225e30..01d23fcfc 100644 --- a/.forge-snapshots/removeLiquidity with native token.snap +++ b/.forge-snapshots/removeLiquidity with native token.snap @@ -1 +1 @@ -117755 \ No newline at end of file +117621 \ No newline at end of file diff --git a/.forge-snapshots/removeLiquidity.snap b/.forge-snapshots/removeLiquidity.snap index 3b931cf37..1970d0bc7 100644 --- a/.forge-snapshots/removeLiquidity.snap +++ b/.forge-snapshots/removeLiquidity.snap @@ -1 +1 @@ -120968 \ No newline at end of file +120834 \ 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 eca48267a..41735a4dd 100644 --- a/.forge-snapshots/simple swap with native.snap +++ b/.forge-snapshots/simple swap with native.snap @@ -1 +1 @@ -131040 \ No newline at end of file +117339 \ No newline at end of file diff --git a/.forge-snapshots/simple swap.snap b/.forge-snapshots/simple swap.snap index 095efb63e..4719fd039 100644 --- a/.forge-snapshots/simple swap.snap +++ b/.forge-snapshots/simple swap.snap @@ -1 +1 @@ -149399 \ No newline at end of file +132578 \ No newline at end of file diff --git a/.forge-snapshots/sparse external sload.snap b/.forge-snapshots/sparse external sload.snap new file mode 100644 index 000000000..cdfeb3679 --- /dev/null +++ b/.forge-snapshots/sparse external sload.snap @@ -0,0 +1 @@ +2023 \ No newline at end of file diff --git a/.forge-snapshots/swap CA custom curve + swap noop.snap b/.forge-snapshots/swap CA custom curve + swap noop.snap index 186a55e8b..612b68dd0 100644 --- a/.forge-snapshots/swap CA custom curve + swap noop.snap +++ b/.forge-snapshots/swap CA custom curve + swap noop.snap @@ -1 +1 @@ -139399 \ No newline at end of file +135860 \ No newline at end of file diff --git a/.forge-snapshots/swap CA fee on unspecified.snap b/.forge-snapshots/swap CA fee on unspecified.snap index 221e1f23e..08f6f6124 100644 --- a/.forge-snapshots/swap CA fee on unspecified.snap +++ b/.forge-snapshots/swap CA fee on unspecified.snap @@ -1 +1 @@ -184879 \ No newline at end of file +184809 \ 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 c5a1d73a5..8b56c7d74 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -113935 \ No newline at end of file +113800 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 7702e4fff..0372bd1c3 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -125313 \ No newline at end of file +125221 \ No newline at end of file diff --git a/.forge-snapshots/swap burn 6909 for input.snap b/.forge-snapshots/swap burn 6909 for input.snap index 19f777efe..9ff1f5c77 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -137318 \ No newline at end of file +137207 \ No newline at end of file diff --git a/.forge-snapshots/swap burn native 6909 for input.snap b/.forge-snapshots/swap burn native 6909 for input.snap index d4bfbdca3..4228ebd6b 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -126418 \ No newline at end of file +126323 \ No newline at end of file diff --git a/.forge-snapshots/swap mint native output as 6909.snap b/.forge-snapshots/swap mint native output as 6909.snap index f97aa25f1..3de9f0d73 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -148453 \ No newline at end of file +148385 \ No newline at end of file diff --git a/.forge-snapshots/swap mint output as 6909.snap b/.forge-snapshots/swap mint output as 6909.snap index d706786b3..f6f049473 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -165348 \ No newline at end of file +165202 \ No newline at end of file diff --git a/.forge-snapshots/swap skips hook call if hook is caller.snap b/.forge-snapshots/swap skips hook call if hook is caller.snap index b7c3d28f5..95777a5a8 100644 --- a/.forge-snapshots/swap skips hook call if hook is caller.snap +++ b/.forge-snapshots/swap skips hook call if hook is caller.snap @@ -1 +1 @@ -225042 \ No newline at end of file +224701 \ 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 c115de177..72f4ce3f4 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -149615 \ No newline at end of file +149469 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index 46fbfabfa..9f4a834a8 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -125325 \ No newline at end of file +125233 \ No newline at end of file diff --git a/.forge-snapshots/swap with lp fee and protocol fee.snap b/.forge-snapshots/swap with lp fee and protocol fee.snap index 0561b4e61..7d3d777bd 100644 --- a/.forge-snapshots/swap with lp fee and protocol fee.snap +++ b/.forge-snapshots/swap with lp fee and protocol fee.snap @@ -1 +1 @@ -181967 \ No newline at end of file +181791 \ 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 1a498be51..cf4bf7d64 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -160258 \ No newline at end of file +160022 \ No newline at end of file diff --git a/.github/workflows/tests-pr.yml b/.github/workflows/tests-pr.yml index d06191042..5eb80cec9 100644 --- a/.github/workflows/tests-pr.yml +++ b/.github/workflows/tests-pr.yml @@ -2,6 +2,9 @@ name: Tests on: pull_request: + branches: + - main + - dev jobs: forge-tests: diff --git a/src/Extsload.sol b/src/Extsload.sol index 776243389..7dd04284f 100644 --- a/src/Extsload.sol +++ b/src/Extsload.sol @@ -25,4 +25,23 @@ abstract contract Extsload is IExtsload { return value; } + + /// @dev since the function is external and enters a new call context and exits right after execution, Solidity's memory management convention can be disregarded and a direct slice of memory can be returned + function extsload(bytes32[] calldata slots) external view returns (bytes32[] memory) { + assembly ("memory-safe") { + // abi offset for dynamic array + mstore(0, 0x20) + mstore(0x20, slots.length) + let end := add(0x40, shl(5, slots.length)) + let memptr := 0x40 + let calldataptr := slots.offset + for {} 1 {} { + mstore(memptr, sload(calldataload(calldataptr))) + memptr := add(memptr, 0x20) + calldataptr := add(calldataptr, 0x20) + if iszero(lt(memptr, end)) { break } + } + return(0, end) + } + } } diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 8b01de5bd..d70dce273 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -17,6 +17,7 @@ import {ProtocolFees} from "./ProtocolFees.sol"; import {ERC6909Claims} from "./ERC6909Claims.sol"; import {PoolId, PoolIdLibrary} from "./types/PoolId.sol"; import {BalanceDelta, BalanceDeltaLibrary, toBalanceDelta} from "./types/BalanceDelta.sol"; +import {BeforeSwapDelta} from "./types/BeforeSwapDelta.sol"; import {Lock} from "./libraries/Lock.sol"; import {CurrencyDelta} from "./libraries/CurrencyDelta.sol"; import {NonZeroDeltaCount} from "./libraries/NonZeroDeltaCount.sol"; @@ -174,7 +175,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim key.hooks.afterInitialize(key, sqrtPriceX96, tick, hookData); - // 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. + // On initialize 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); } @@ -270,7 +271,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim PoolId id = key.toId(); _checkPoolInitialized(id); - (int256 amountToSwap, int128 hookDeltaSpecified) = key.hooks.beforeSwap(key, params, hookData); + (int256 amountToSwap, BeforeSwapDelta beforeSwapDelta) = key.hooks.beforeSwap(key, params, hookData); // execute swap, account protocol fees, and emit swap event swapDelta = _swap( @@ -285,7 +286,7 @@ contract PoolManager is IPoolManager, ProtocolFees, NoDelegateCall, ERC6909Claim ); BalanceDelta hookDelta; - (swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, hookDeltaSpecified); + (swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta); // if the hook doesnt have the flag to be able to return deltas, hookDelta will always be 0 if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); diff --git a/src/interfaces/IExtsload.sol b/src/interfaces/IExtsload.sol index 3b325008d..c11e75dba 100644 --- a/src/interfaces/IExtsload.sol +++ b/src/interfaces/IExtsload.sol @@ -12,4 +12,9 @@ interface IExtsload { /// @param nSlots Number of slots to load into return value /// @return value The value of the sload-ed slots concatenated as dynamic bytes function extsload(bytes32 slot, uint256 nSlots) external view returns (bytes memory value); + + /// @notice Called by external contracts to access sparse pool state + /// @param slots List of slots to SLOAD from. + /// @return values List of loaded values. + function extsload(bytes32[] calldata slots) external view returns (bytes32[] memory values); } diff --git a/src/interfaces/IHooks.sol b/src/interfaces/IHooks.sol index 2e64acb3e..a3b3de193 100644 --- a/src/interfaces/IHooks.sol +++ b/src/interfaces/IHooks.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {PoolKey} from "../types/PoolKey.sol"; import {BalanceDelta} from "../types/BalanceDelta.sol"; import {IPoolManager} from "./IPoolManager.sol"; +import {BeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; /// @notice The PoolManager contract decides whether to invoke specific hooks by inspecting the leading bits /// of the hooks contract address. For example, a 1 bit in the first bit of the address will @@ -97,13 +98,13 @@ interface IHooks { /// @param params The parameters for the swap /// @param hookData Arbitrary data handed into the PoolManager by the swapper to be be passed on to the hook /// @return bytes4 The function selector for the hook - /// @return int128 The hook's delta in specified currency. Positive: the hook is owed/took currency, negative: the hook owes/sent currency + /// @return BeforeSwapDelta The hook's delta in specified and unspecified currencies. Positive: the hook is owed/took currency, negative: the hook owes/sent currency function beforeSwap( address sender, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata hookData - ) external returns (bytes4, int128); + ) external returns (bytes4, BeforeSwapDelta); /// @notice The hook called after a swap /// @param sender The initial msg.sender for the swap call diff --git a/src/libraries/Hooks.sol b/src/libraries/Hooks.sol index a6cefc69a..5c6d9dff2 100644 --- a/src/libraries/Hooks.sol +++ b/src/libraries/Hooks.sol @@ -6,6 +6,7 @@ import {IHooks} from "../interfaces/IHooks.sol"; import {SafeCast} from "../libraries/SafeCast.sol"; import {LPFeeLibrary} from "./LPFeeLibrary.sol"; import {BalanceDelta, toBalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; /// @notice V4 decides whether to invoke specific hooks by inspecting the leading bits of the address that @@ -16,6 +17,7 @@ library Hooks { using LPFeeLibrary for uint24; using Hooks for IHooks; using SafeCast for int256; + using BeforeSwapDeltaLibrary for BeforeSwapDelta; uint256 internal constant BEFORE_INITIALIZE_FLAG = 1 << 159; uint256 internal constant AFTER_INITIALIZE_FLAG = 1 << 158; @@ -141,6 +143,7 @@ library Hooks { { bytes memory result = callHook(self, data); + // If this hook wasnt meant to return something, default to 0 delta if (!parseReturn) return 0; (, delta) = abi.decode(result, (bytes4, int256)); } @@ -233,21 +236,31 @@ library Hooks { /// @notice calls beforeSwap hook if permissioned and validates return value function beforeSwap(IHooks self, PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) internal - returns (int256 amountToSwap, int128 hookDeltaSpecified) + returns (int256 amountToSwap, BeforeSwapDelta hookReturn) { amountToSwap = params.amountSpecified; - if (msg.sender == address(self)) return (amountToSwap, hookDeltaSpecified); + if (msg.sender == address(self)) return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA); + if (self.hasPermission(BEFORE_SWAP_FLAG)) { - hookDeltaSpecified = self.callHookWithReturnDelta( - abi.encodeWithSelector(IHooks.beforeSwap.selector, msg.sender, key, params, hookData), - self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG) - ).toInt128(); + bool canReturnDelta = self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG); + hookReturn = BeforeSwapDelta.wrap( + self.callHookWithReturnDelta( + abi.encodeWithSelector(IHooks.beforeSwap.selector, msg.sender, key, params, hookData), + canReturnDelta + ) + ); - // Update the swap amount according to the hook's return, and check that the swap type doesnt change (exact input/output) - if (hookDeltaSpecified != 0) { - bool exactInput = amountToSwap < 0; - amountToSwap += hookDeltaSpecified; - if (exactInput ? amountToSwap > 0 : amountToSwap < 0) revert HookDeltaExceedsSwapAmount(); + // skip this logic for the case where the hook return is 0 + if (canReturnDelta) { + // any return in unspecified is passed to the afterSwap hook for handling + int128 hookDeltaSpecified = hookReturn.getSpecifiedDelta(); + + // Update the swap amount according to the hook's return, and check that the swap type doesnt change (exact input/output) + if (hookDeltaSpecified != 0) { + bool exactInput = amountToSwap < 0; + amountToSwap += hookDeltaSpecified; + if (exactInput ? amountToSwap > 0 : amountToSwap < 0) revert HookDeltaExceedsSwapAmount(); + } } } } @@ -259,27 +272,30 @@ library Hooks { IPoolManager.SwapParams memory params, BalanceDelta swapDelta, bytes calldata hookData, - int128 hookDeltaSpecified - ) internal returns (BalanceDelta swapperDelta, BalanceDelta hookDelta) { + BeforeSwapDelta beforeSwapHookReturn + ) internal returns (BalanceDelta, BalanceDelta) { if (msg.sender == address(self)) return (swapDelta, BalanceDeltaLibrary.ZERO_DELTA); - int128 hookDeltaUnspecified; - swapperDelta = swapDelta; + int128 hookDeltaSpecified = beforeSwapHookReturn.getSpecifiedDelta(); + int128 hookDeltaUnspecified = beforeSwapHookReturn.getUnspecifiedDelta(); + if (self.hasPermission(AFTER_SWAP_FLAG)) { - hookDeltaUnspecified = self.callHookWithReturnDelta( + hookDeltaUnspecified += self.callHookWithReturnDelta( abi.encodeWithSelector(IHooks.afterSwap.selector, msg.sender, key, params, swapDelta, hookData), self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG) ).toInt128(); } + BalanceDelta hookDelta; if (hookDeltaUnspecified != 0 || hookDeltaSpecified != 0) { hookDelta = (params.amountSpecified < 0 == params.zeroForOne) ? toBalanceDelta(hookDeltaSpecified, hookDeltaUnspecified) : toBalanceDelta(hookDeltaUnspecified, hookDeltaSpecified); // the caller has to pay for (or receive) the hook's delta - swapperDelta = swapDelta - hookDelta; + swapDelta = swapDelta - hookDelta; } + return (swapDelta, hookDelta); } /// @notice calls beforeDonate hook if permissioned and validates return value diff --git a/src/libraries/Pool.sol b/src/libraries/Pool.sol index b6ed38594..d6ed9ac68 100644 --- a/src/libraries/Pool.sol +++ b/src/libraries/Pool.sol @@ -217,8 +217,7 @@ library Pool { } if (liquidityDelta != 0) { - int24 tick = self.slot0.tick; - uint160 sqrtPriceX96 = self.slot0.sqrtPriceX96; + (int24 tick, uint160 sqrtPriceX96) = (self.slot0.tick, self.slot0.sqrtPriceX96); if (tick < tickLower) { // current tick is below the passed range; liquidity can only become in range by crossing from left to // right, when we'll need _more_ currency0 (it's becoming more valuable) so user must provide it @@ -305,7 +304,6 @@ library Pool { { Slot0 memory slot0Start = self.slot0; bool zeroForOne = params.zeroForOne; - bool exactInput = params.amountSpecified < 0; SwapCache memory cache = SwapCache({ liquidityStart: self.liquidity, @@ -322,6 +320,8 @@ library Pool { swapFee = cache.protocolFee == 0 ? slot0Start.lpFee : uint24(cache.protocolFee).calculateSwapFee(slot0Start.lpFee); + bool exactInput = params.amountSpecified < 0; + if (!exactInput && (swapFee == LPFeeLibrary.MAX_LP_FEE)) { revert InvalidFeeForExactOut(); } @@ -347,16 +347,17 @@ library Pool { StepComputations memory step; // continue swapping as long as we haven't used the entire input/output and haven't reached the price limit - while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) { + while (!(state.amountSpecifiedRemaining == 0 || state.sqrtPriceX96 == params.sqrtPriceLimitX96)) { step.sqrtPriceStartX96 = state.sqrtPriceX96; (step.tickNext, step.initialized) = self.tickBitmap.nextInitializedTickWithinOneWord(state.tick, params.tickSpacing, zeroForOne); // ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds - if (step.tickNext < TickMath.MIN_TICK) { + if (step.tickNext <= TickMath.MIN_TICK) { step.tickNext = TickMath.MIN_TICK; - } else if (step.tickNext > TickMath.MAX_TICK) { + } + if (step.tickNext >= TickMath.MAX_TICK) { step.tickNext = TickMath.MAX_TICK; } @@ -376,17 +377,17 @@ library Pool { swapFee ); - if (exactInput) { - // safe because we test that amountSpecified > amountIn + feeAmount in SwapMath + if (!exactInput) { unchecked { - state.amountSpecifiedRemaining += (step.amountIn + step.feeAmount).toInt256(); + state.amountSpecifiedRemaining -= step.amountOut.toInt256(); } - state.amountCalculated = state.amountCalculated + step.amountOut.toInt256(); + state.amountCalculated = state.amountCalculated - (step.amountIn + step.feeAmount).toInt256(); } else { + // safe because we test that amountSpecified > amountIn + feeAmount in SwapMath unchecked { - state.amountSpecifiedRemaining -= step.amountOut.toInt256(); + state.amountSpecifiedRemaining += (step.amountIn + step.feeAmount).toInt256(); } - state.amountCalculated = state.amountCalculated - (step.amountIn + step.feeAmount).toInt256(); + state.amountCalculated = state.amountCalculated + step.amountOut.toInt256(); } // if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee @@ -413,15 +414,9 @@ library Pool { if (state.sqrtPriceX96 == step.sqrtPriceNextX96) { // if the tick is initialized, run the tick transition if (step.initialized) { - uint256 feeGrowthGlobal0X128; - uint256 feeGrowthGlobal1X128; - if (!zeroForOne) { - feeGrowthGlobal0X128 = self.feeGrowthGlobal0X128; - feeGrowthGlobal1X128 = state.feeGrowthGlobalX128; - } else { - feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; - feeGrowthGlobal1X128 = self.feeGrowthGlobal1X128; - } + (uint256 feeGrowthGlobal0X128, uint256 feeGrowthGlobal1X128) = zeroForOne + ? (state.feeGrowthGlobalX128, self.feeGrowthGlobal1X128) + : (self.feeGrowthGlobal0X128, state.feeGrowthGlobalX128); int128 liquidityNet = Pool.crossTick(self, step.tickNext, feeGrowthGlobal0X128, feeGrowthGlobal1X128); // if we're moving leftward, we interpret liquidityNet as the opposite sign @@ -431,8 +426,14 @@ library Pool { state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet); } + // Equivalent to `state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;` unchecked { - state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext; + // cannot cast a bool to an int24 in Solidity + int24 _zeroForOne; + assembly { + _zeroForOne := zeroForOne + } + state.tick = step.tickNext - _zeroForOne; } } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved @@ -440,28 +441,28 @@ library Pool { } } - (self.slot0.sqrtPriceX96, self.slot0.tick) = (state.sqrtPriceX96, state.tick); + (self.slot0.tick, self.slot0.sqrtPriceX96) = (state.tick, state.sqrtPriceX96); // update liquidity if it changed if (cache.liquidityStart != state.liquidity) self.liquidity = state.liquidity; // update fee growth global - if (zeroForOne) { - self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; - } else { + if (!zeroForOne) { self.feeGrowthGlobal1X128 = state.feeGrowthGlobalX128; + } else { + self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128; } unchecked { - if (zeroForOne == exactInput) { + if (zeroForOne != exactInput) { result = toBalanceDelta( - (params.amountSpecified - state.amountSpecifiedRemaining).toInt128(), - state.amountCalculated.toInt128() + state.amountCalculated.toInt128(), + (params.amountSpecified - state.amountSpecifiedRemaining).toInt128() ); } else { result = toBalanceDelta( - state.amountCalculated.toInt128(), - (params.amountSpecified - state.amountSpecifiedRemaining).toInt128() + (params.amountSpecified - state.amountSpecifiedRemaining).toInt128(), + state.amountCalculated.toInt128() ); } } @@ -471,8 +472,9 @@ library Pool { function donate(State storage state, uint256 amount0, uint256 amount1) internal returns (BalanceDelta delta) { uint128 liquidity = state.liquidity; if (liquidity == 0) revert NoLiquidityToReceiveFees(); - delta = toBalanceDelta(-(amount0.toInt128()), -(amount1.toInt128())); unchecked { + // negation safe as amount0 and amount1 are always positive + delta = toBalanceDelta(-(amount0.toInt128()), -(amount1.toInt128())); if (amount0 > 0) { state.feeGrowthGlobal0X128 += FullMath.mulDiv(amount0, FixedPoint128.Q128, liquidity); } diff --git a/src/libraries/SafeCast.sol b/src/libraries/SafeCast.sol index 5eb5bd474..8f22bed45 100644 --- a/src/libraries/SafeCast.sol +++ b/src/libraries/SafeCast.sol @@ -6,35 +6,55 @@ pragma solidity ^0.8.20; library SafeCast { error SafeCastOverflow(); + function _revertOverflow() private pure { + /// @solidity memory-safe-assembly + assembly { + // Store the function selector of `SafeCastOverflow()`. + mstore(0x00, 0x93dafdf1) + // Revert with (offset, size). + revert(0x1c, 0x04) + } + } + /// @notice Cast a uint256 to a uint160, revert on overflow - /// @param y The uint256 to be downcasted - /// @return z The downcasted integer, now type uint160 - function toUint160(uint256 y) internal pure returns (uint160 z) { - z = uint160(y); - if (z != y) revert SafeCastOverflow(); + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type uint160 + function toUint160(uint256 x) internal pure returns (uint160) { + if (x >= 1 << 160) _revertOverflow(); + return uint160(x); + } + + /// @notice Cast a uint256 to a uint128, revert on overflow + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type uint128 + function toUint128(uint256 x) internal pure returns (uint128) { + if (x >= 1 << 128) _revertOverflow(); + return uint128(x); } /// @notice Cast a int256 to a int128, revert on overflow or underflow - /// @param y The int256 to be downcasted - /// @return z The downcasted integer, now type int128 - function toInt128(int256 y) internal pure returns (int128 z) { - z = int128(y); - if (z != y) revert SafeCastOverflow(); + /// @param x The int256 to be downcasted + /// @return The downcasted integer, now type int128 + function toInt128(int256 x) internal pure returns (int128) { + unchecked { + if (((1 << 127) + uint256(x)) >> 128 == uint256(0)) return int128(x); + _revertOverflow(); + } } /// @notice Cast a uint256 to a int256, revert on overflow - /// @param y The uint256 to be casted - /// @return z The casted integer, now type int256 - function toInt256(uint256 y) internal pure returns (int256 z) { - if (y > uint256(type(int256).max)) revert SafeCastOverflow(); - z = int256(y); + /// @param x The uint256 to be casted + /// @return The casted integer, now type int256 + function toInt256(uint256 x) internal pure returns (int256) { + if (int256(x) >= 0) return int256(x); + _revertOverflow(); } /// @notice Cast a uint256 to a int128, revert on overflow - /// @param y The uint256 to be downcasted - /// @return z The downcasted integer, now type int128 - function toInt128(uint256 y) internal pure returns (int128 z) { - if (y > uint128(type(int128).max)) revert SafeCastOverflow(); - z = int128(int256(y)); + /// @param x The uint256 to be downcasted + /// @return The downcasted integer, now type int128 + function toInt128(uint256 x) internal pure returns (int128) { + if (x >= 1 << 127) _revertOverflow(); + return int128(int256(x)); } } diff --git a/src/test/BaseTestHooks.sol b/src/test/BaseTestHooks.sol index be791a6e7..061887dc2 100644 --- a/src/test/BaseTestHooks.sol +++ b/src/test/BaseTestHooks.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.24; import {IHooks} from "../interfaces/IHooks.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {BalanceDelta} from "../types/BalanceDelta.sol"; +import {BeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; contract BaseTestHooks is IHooks { @@ -71,7 +72,7 @@ contract BaseTestHooks is IHooks { PoolKey calldata, /* key **/ IPoolManager.SwapParams calldata, /* params **/ bytes calldata /* hookData **/ - ) external virtual returns (bytes4, int128) { + ) external virtual returns (bytes4, BeforeSwapDelta) { revert HookNotImplemented(); } diff --git a/src/test/CustomCurveHook.sol b/src/test/CustomCurveHook.sol index ab7f2725e..1135e380d 100644 --- a/src/test/CustomCurveHook.sol +++ b/src/test/CustomCurveHook.sol @@ -6,7 +6,8 @@ import {SafeCast} from "../libraries/SafeCast.sol"; import {IHooks} from "../interfaces/IHooks.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {PoolKey} from "../types/PoolKey.sol"; -import {BalanceDelta, toBalanceDelta} from "../types/BalanceDelta.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "../types/BalanceDelta.sol"; import {Currency} from "../types/Currency.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; import {BaseTestHooks} from "./BaseTestHooks.sol"; @@ -36,7 +37,7 @@ contract CustomCurveHook is BaseTestHooks { PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata /* hookData **/ - ) external override onlyPoolManager returns (bytes4, int128) { + ) external override onlyPoolManager returns (bytes4, BeforeSwapDelta) { (Currency inputCurrency, Currency outputCurrency, uint256 amount) = _getInputOutputAndAmount(key, params); // this "custom curve" is a line, 1-1 @@ -44,18 +45,9 @@ contract CustomCurveHook is BaseTestHooks { manager.take(inputCurrency, address(this), amount); outputCurrency.settle(manager, address(this), amount, false); - // return -amountSpecified to no-op the concentrated liquidity swap - return (IHooks.beforeSwap.selector, int128(-params.amountSpecified)); - } - - function afterSwap( - address, /* sender **/ - PoolKey calldata, /* key **/ - IPoolManager.SwapParams calldata params, - BalanceDelta, /* delta **/ - bytes calldata /* hookData **/ - ) external view override onlyPoolManager returns (bytes4, int128) { - return (IHooks.afterSwap.selector, int128(params.amountSpecified)); + // return -amountSpecified as specified to no-op the concentrated liquidity swap + BeforeSwapDelta hookDelta = toBeforeSwapDelta(int128(-params.amountSpecified), int128(params.amountSpecified)); + return (IHooks.beforeSwap.selector, hookDelta); } function afterAddLiquidity( diff --git a/src/test/DeltaReturningHook.sol b/src/test/DeltaReturningHook.sol index 9a093caf6..d8b7e2d9e 100644 --- a/src/test/DeltaReturningHook.sol +++ b/src/test/DeltaReturningHook.sol @@ -12,6 +12,7 @@ import {Currency} from "../types/Currency.sol"; import {BaseTestHooks} from "./BaseTestHooks.sol"; import {IERC20Minimal} from "../interfaces/external/IERC20Minimal.sol"; import {CurrencyLibrary, Currency} from "../types/Currency.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "../types/BeforeSwapDelta.sol"; contract DeltaReturningHook is BaseTestHooks { using Hooks for IHooks; @@ -21,7 +22,8 @@ contract DeltaReturningHook is BaseTestHooks { IPoolManager immutable manager; int128 deltaSpecified; - int128 deltaUnspecified; + int128 deltaUnspecifiedBeforeSwap; + int128 deltaUnspecifiedAfterSwap; constructor(IPoolManager _manager) { manager = _manager; @@ -36,8 +38,12 @@ contract DeltaReturningHook is BaseTestHooks { deltaSpecified = delta; } - function setDeltaUnspecified(int128 delta) external { - deltaUnspecified = delta; + function setDeltaUnspecifiedBeforeSwap(int128 delta) external { + deltaUnspecifiedBeforeSwap = delta; + } + + function setDeltaUnspecifiedAfterSwap(int128 delta) external { + deltaUnspecifiedAfterSwap = delta; } function beforeSwap( @@ -45,11 +51,15 @@ contract DeltaReturningHook is BaseTestHooks { PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata /* hookData **/ - ) external override onlyPoolManager returns (bytes4, int128) { - (Currency specifiedCurrency,) = _sortCurrencies(key, params); - _settleOrTake(specifiedCurrency, deltaSpecified); + ) external override onlyPoolManager returns (bytes4, BeforeSwapDelta) { + (Currency specifiedCurrency, Currency unspecifiedCurrency) = _sortCurrencies(key, params); + + if (deltaSpecified != 0) _settleOrTake(specifiedCurrency, deltaSpecified); + if (deltaUnspecifiedBeforeSwap != 0) _settleOrTake(unspecifiedCurrency, deltaUnspecifiedBeforeSwap); + + BeforeSwapDelta beforeSwapDelta = toBeforeSwapDelta(deltaSpecified, deltaUnspecifiedBeforeSwap); - return (IHooks.beforeSwap.selector, deltaSpecified); + return (IHooks.beforeSwap.selector, beforeSwapDelta); } function afterSwap( @@ -60,9 +70,9 @@ contract DeltaReturningHook is BaseTestHooks { bytes calldata /* hookData **/ ) external override onlyPoolManager returns (bytes4, int128) { (, Currency unspecifiedCurrency) = _sortCurrencies(key, params); - _settleOrTake(unspecifiedCurrency, deltaUnspecified); + _settleOrTake(unspecifiedCurrency, deltaUnspecifiedAfterSwap); - return (IHooks.afterSwap.selector, deltaUnspecified); + return (IHooks.afterSwap.selector, deltaUnspecifiedAfterSwap); } function _sortCurrencies(PoolKey calldata key, IPoolManager.SwapParams calldata params) diff --git a/src/test/DynamicFeesTestHook.sol b/src/test/DynamicFeesTestHook.sol index c6355d4b9..f0fc00f0e 100644 --- a/src/test/DynamicFeesTestHook.sol +++ b/src/test/DynamicFeesTestHook.sol @@ -5,6 +5,7 @@ import {BaseTestHooks} from "./BaseTestHooks.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {IHooks} from "../interfaces/IHooks.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; contract DynamicFeesTestHook is BaseTestHooks { uint24 internal fee; @@ -30,10 +31,10 @@ contract DynamicFeesTestHook is BaseTestHooks { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) external override - returns (bytes4, int128) + returns (bytes4, BeforeSwapDelta) { manager.updateDynamicLPFee(key, fee); - return (IHooks.beforeSwap.selector, 0); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA); } function forcePoolFeeUpdate(PoolKey calldata _key, uint24 _fee) external { diff --git a/src/test/EmptyTestHooks.sol b/src/test/EmptyTestHooks.sol index 0b3ed4128..9f49042e2 100644 --- a/src/test/EmptyTestHooks.sol +++ b/src/test/EmptyTestHooks.sol @@ -6,6 +6,7 @@ import {IHooks} from "../interfaces/IHooks.sol"; import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; contract EmptyTestHooks is IHooks { using Hooks for IHooks; @@ -91,9 +92,9 @@ contract EmptyTestHooks is IHooks { external pure override - returns (bytes4, int128) + returns (bytes4, BeforeSwapDelta) { - return (IHooks.beforeSwap.selector, 0); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA); } function afterSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) diff --git a/src/test/MockHooks.sol b/src/test/MockHooks.sol index e7dc57f09..07f5af50d 100644 --- a/src/test/MockHooks.sol +++ b/src/test/MockHooks.sol @@ -7,6 +7,7 @@ import {IPoolManager} from "../interfaces/IPoolManager.sol"; import {PoolKey} from "../types/PoolKey.sol"; import {BalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; import {PoolId, PoolIdLibrary} from "../types/PoolId.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; contract MockHooks is IHooks { using PoolIdLibrary for PoolKey; @@ -96,11 +97,12 @@ contract MockHooks is IHooks { function beforeSwap(address, PoolKey calldata, IPoolManager.SwapParams calldata, bytes calldata hookData) external override - returns (bytes4, int128) + returns (bytes4, BeforeSwapDelta) { beforeSwapData = hookData; bytes4 selector = MockHooks.beforeSwap.selector; - return (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], 0); + return + (returnValues[selector] == bytes4(0) ? selector : returnValues[selector], BeforeSwapDeltaLibrary.ZERO_DELTA); } function afterSwap( diff --git a/src/test/PoolSwapTest.sol b/src/test/PoolSwapTest.sol index 5bb773e8b..0664a3d02 100644 --- a/src/test/PoolSwapTest.sol +++ b/src/test/PoolSwapTest.sol @@ -8,8 +8,6 @@ import {PoolKey} from "../types/PoolKey.sol"; import {IHooks} from "../interfaces/IHooks.sol"; import {Hooks} from "../libraries/Hooks.sol"; import {PoolTestBase} from "./PoolTestBase.sol"; -import {Hooks} from "../libraries/Hooks.sol"; -import {IHooks} from "../interfaces/IHooks.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; contract PoolSwapTest is PoolTestBase { @@ -75,7 +73,7 @@ contract PoolSwapTest is PoolTestBase { require(deltaAfter1 >= 0, "deltaAfter1 is not greater than or equal to 0"); } else { // exact output, 0 for 1 - require(deltaAfter0 <= 0, "deltaAfter0 is not less than zero"); + require(deltaAfter0 <= 0, "deltaAfter0 is not less than or equal to zero"); require(delta.amount1() == deltaAfter1, "delta.amount1() is not equal to deltaAfter1"); require( deltaAfter1 <= data.params.amountSpecified, @@ -93,7 +91,7 @@ contract PoolSwapTest is PoolTestBase { require(deltaAfter0 >= 0, "deltaAfter0 is not greater than or equal to 0"); } else { // exact output, 1 for 0 - require(deltaAfter1 <= 0, "deltaAfter1 is not less than 0"); + require(deltaAfter1 <= 0, "deltaAfter1 is not less than or equal to 0"); require(delta.amount0() == deltaAfter0, "delta.amount0() is not equal to deltaAfter0"); require( deltaAfter0 <= data.params.amountSpecified, diff --git a/src/test/SkipCallsTestHook.sol b/src/test/SkipCallsTestHook.sol index 7d0a8f2c9..50035ca96 100644 --- a/src/test/SkipCallsTestHook.sol +++ b/src/test/SkipCallsTestHook.sol @@ -14,6 +14,7 @@ import {PoolTestBase} from "./PoolTestBase.sol"; import {Constants} from "../../test/utils/Constants.sol"; import {Test} from "forge-std/Test.sol"; import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "../types/BeforeSwapDelta.sol"; contract SkipCallsTestHook is BaseTestHooks, Test { using CurrencySettleTake for Currency; @@ -96,11 +97,11 @@ contract SkipCallsTestHook is BaseTestHooks, Test { function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata hookData) external override - returns (bytes4, int128) + returns (bytes4, BeforeSwapDelta) { counter++; _swap(key, params, hookData); - return (IHooks.beforeSwap.selector, 0); + return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA); } function afterSwap( diff --git a/src/test/SwapRouterNoChecks.sol b/src/test/SwapRouterNoChecks.sol new file mode 100644 index 000000000..9c00976d8 --- /dev/null +++ b/src/test/SwapRouterNoChecks.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +import {CurrencyLibrary, Currency} from "../types/Currency.sol"; +import {IPoolManager} from "../interfaces/IPoolManager.sol"; +import {BalanceDelta, BalanceDeltaLibrary} from "../types/BalanceDelta.sol"; +import {PoolKey} from "../types/PoolKey.sol"; +import {IHooks} from "../interfaces/IHooks.sol"; +import {Hooks} from "../libraries/Hooks.sol"; +import {PoolTestBase} from "./PoolTestBase.sol"; +import {CurrencySettleTake} from "../libraries/CurrencySettleTake.sol"; + +contract SwapRouterNoChecks is PoolTestBase { + using CurrencyLibrary for Currency; + using CurrencySettleTake for Currency; + using Hooks for IHooks; + + constructor(IPoolManager _manager) PoolTestBase(_manager) {} + + error NoSwapOccurred(); + + struct CallbackData { + address sender; + PoolKey key; + IPoolManager.SwapParams params; + } + + function swap(PoolKey memory key, IPoolManager.SwapParams memory params) external payable { + manager.unlock(abi.encode(CallbackData(msg.sender, key, params))); + } + + function unlockCallback(bytes calldata rawData) external returns (bytes memory) { + require(msg.sender == address(manager)); + + CallbackData memory data = abi.decode(rawData, (CallbackData)); + + BalanceDelta delta = manager.swap(data.key, data.params, new bytes(0)); + + if (data.params.zeroForOne) { + data.key.currency0.settle(manager, data.sender, uint256(int256(-delta.amount0())), false); + data.key.currency1.take(manager, data.sender, uint256(int256(delta.amount1())), false); + } else { + data.key.currency1.settle(manager, data.sender, uint256(int256(-delta.amount1())), false); + data.key.currency0.take(manager, data.sender, uint256(int256(delta.amount0())), false); + } + } +} diff --git a/src/types/BeforeSwapDelta.sol b/src/types/BeforeSwapDelta.sol new file mode 100644 index 000000000..d7b7bdd6c --- /dev/null +++ b/src/types/BeforeSwapDelta.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {BalanceDelta} from "./BalanceDelta.sol"; + +// Return type of the beforeSwap hook. +// Upper 128 bits is the delta in specified tokens. Lower 128 bits is delta in unspecified tokens (to match the afterSwap hook) +type BeforeSwapDelta is int256; + +// Creates a BeforeSwapDelta from specified and unspecified +function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified) + pure + returns (BeforeSwapDelta beforeSwapDelta) +{ + /// @solidity memory-safe-assembly + assembly { + beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified)) + } +} + +library BeforeSwapDeltaLibrary { + BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0); + + /// extracts int128 from the upper 128 bits of the BeforeSwapDelta + /// returned by beforeSwap + function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaSpecified) { + assembly { + deltaSpecified := sar(128, delta) + } + } + + /// extracts int128 from the lower 128 bits of the BeforeSwapDelta + /// returned by beforeSwap and afterSwap + function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaUnspecified) { + /// @solidity memory-safe-assembly + assembly { + deltaUnspecified := signextend(15, delta) + } + } +} diff --git a/test/Extsload.t.sol b/test/Extsload.t.sol new file mode 100644 index 000000000..36e928a6e --- /dev/null +++ b/test/Extsload.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {Extsload} from "../src/Extsload.sol"; +import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; + +contract Loadable is Extsload {} + +/// @author philogy +contract ExtsloadTest is Test, GasSnapshot { + Loadable loadable = new Loadable(); + + function test_load10_sparse() public { + bytes32[] memory keys = new bytes32[](10); + for (uint256 i = 0; i < keys.length; i++) { + keys[i] = keccak256(abi.encode(i)); + vm.store(address(loadable), keys[i], bytes32(i)); + } + + bytes32[] memory values = loadable.extsload(keys); + snapLastCall("sparse external sload"); + assertEq(values.length, keys.length); + for (uint256 i = 0; i < values.length; i++) { + assertEq(values[i], bytes32(i)); + } + } +} diff --git a/test/NoDelegateCall.t.sol b/test/NoDelegateCall.t.sol index b6f688476..4b865841c 100644 --- a/test/NoDelegateCall.t.sol +++ b/test/NoDelegateCall.t.sol @@ -4,44 +4,43 @@ pragma solidity ^0.8.20; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {Test} from "forge-std/Test.sol"; import {NoDelegateCallTest} from "../src/test/NoDelegateCallTest.sol"; +import {NoDelegateCall} from "../src/NoDelegateCall.sol"; contract TestDelegateCall is Test, GasSnapshot { - error DelegateCallNotAllowed(); - NoDelegateCallTest noDelegateCallTest; function setUp() public { noDelegateCallTest = new NoDelegateCallTest(); } - function testGasOverhead() public { + function test_gas_noDelegateCall() public { snap( - "NoDelegateCallOverhead", + "NoDelegateCall", noDelegateCallTest.getGasCostOfCannotBeDelegateCalled() - noDelegateCallTest.getGasCostOfCanBeDelegateCalled() ); } - function testDelegateCallNoModifier() public { + function test_delegateCallNoModifier() public { (bool success,) = address(noDelegateCallTest).delegatecall(abi.encode(noDelegateCallTest.canBeDelegateCalled.selector)); assertTrue(success); } - function testDelegateCallWithModifier() public { - vm.expectRevert(DelegateCallNotAllowed.selector); + function test_delegateCallWithModifier_revertsWithDelegateCallNotAllowed() public { + vm.expectRevert(NoDelegateCall.DelegateCallNotAllowed.selector); (bool success,) = address(noDelegateCallTest).delegatecall(abi.encode(noDelegateCallTest.cannotBeDelegateCalled.selector)); // note vm.expectRevert inverts success, so a true result here means it reverted assertTrue(success); } - function testCanCallIntoPrivateMethodWithModifier() public view { + function test_externalCallToPrivateMethodWithModifer_succeeds() public view { noDelegateCallTest.callsIntoNoDelegateCallFunction(); } - function testCannotDelegateCallPrivateMethodWithModifier() public { - vm.expectRevert(DelegateCallNotAllowed.selector); + function test_delegateCallFromExternalToPrivateMethodWithModifier_revertsWithDelegateCallNotAllowed() public { + vm.expectRevert(NoDelegateCall.DelegateCallNotAllowed.selector); (bool success,) = address(noDelegateCallTest).delegatecall( abi.encode(noDelegateCallTest.callsIntoNoDelegateCallFunction.selector) ); diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 8df6ddbcf..3d063462c 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -638,7 +638,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); } - function test_swap_gas() public { + function test_swap_succeeds() public { IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); @@ -646,10 +646,17 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap(key, swapParams, testSettings, ZERO_BYTES); + } + + function test_swap_gas() public { + IPoolManager.SwapParams memory swapParams = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); + + swapRouterNoChecks.swap(key, swapParams); snapLastCall("simple swap"); } - function test_swap_withNative_gas() public { + function test_swap_withNative_succeeds() public { IPoolManager.SwapParams memory swapParams = IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); @@ -657,6 +664,13 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); swapRouter.swap{value: 100}(nativeKey, swapParams, testSettings, ZERO_BYTES); + } + + function test_swap_withNative_gas() public { + IPoolManager.SwapParams memory swapParams = + IPoolManager.SwapParams({zeroForOne: true, amountSpecified: -100, sqrtPriceLimitX96: SQRT_PRICE_1_2}); + + swapRouterNoChecks.swap{value: 100}(nativeKey, swapParams); snapLastCall("simple swap with native"); } @@ -862,12 +876,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_swap_beforeSwapNoOpsSwap_exactInput() public { - address hookAddr = address( - uint160( - Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_SWAP_FLAG - | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - ) - ); + address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); CustomCurveHook impl = new CustomCurveHook(manager); vm.etch(hookAddr, address(impl).code); @@ -896,12 +905,7 @@ contract PoolManagerTest is Test, Deployers, GasSnapshot { } function test_swap_beforeSwapNoOpsSwap_exactOutput() public { - address hookAddr = address( - uint160( - Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG | Hooks.BEFORE_SWAP_FLAG - | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG - ) - ); + address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); CustomCurveHook impl = new CustomCurveHook(manager); vm.etch(hookAddr, address(impl).code); diff --git a/test/libraries/SafeCast.t.sol b/test/libraries/SafeCast.t.sol index bc18e2970..40d873f03 100644 --- a/test/libraries/SafeCast.t.sol +++ b/test/libraries/SafeCast.t.sol @@ -22,6 +22,22 @@ contract SafeCastTest is Test { SafeCast.toUint160(type(uint160).max + uint256(1)); } + function test_fuzz_toUint128(uint256 x) public { + if (x <= type(uint128).max) { + assertEq(uint256(SafeCast.toUint128(x)), x); + } else { + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + SafeCast.toUint128(x); + } + } + + function test_toUint128() public { + assertEq(uint256(SafeCast.toUint128(0)), 0); + assertEq(uint256(SafeCast.toUint128(type(uint128).max)), type(uint128).max); + vm.expectRevert(SafeCast.SafeCastOverflow.selector); + SafeCast.toUint128(type(uint128).max + uint256(1)); + } + function test_fuzz_toInt128_fromInt256(int256 x) public { if (x <= type(int128).max && x >= type(int128).min) { assertEq(int256(SafeCast.toInt128(x)), x); diff --git a/test/utils/Deployers.sol b/test/utils/Deployers.sol index 17b8c7b21..9a0df5ed4 100644 --- a/test/utils/Deployers.sol +++ b/test/utils/Deployers.sol @@ -16,6 +16,7 @@ import {Constants} from "../utils/Constants.sol"; import {SortTokens} from "./SortTokens.sol"; import {PoolModifyLiquidityTest} from "../../src/test/PoolModifyLiquidityTest.sol"; import {PoolSwapTest} from "../../src/test/PoolSwapTest.sol"; +import {SwapRouterNoChecks} from "../../src/test/SwapRouterNoChecks.sol"; import {PoolDonateTest} from "../../src/test/PoolDonateTest.sol"; import {PoolNestedActionsTest} from "../../src/test/PoolNestedActionsTest.sol"; import {PoolTakeTest} from "../../src/test/PoolTakeTest.sol"; @@ -55,6 +56,7 @@ contract Deployers { Currency internal currency1; IPoolManager manager; PoolModifyLiquidityTest modifyLiquidityRouter; + SwapRouterNoChecks swapRouterNoChecks; PoolSwapTest swapRouter; PoolDonateTest donateRouter; PoolTakeTest takeRouter; @@ -93,6 +95,7 @@ contract Deployers { function deployFreshManagerAndRouters() internal { deployFreshManager(); swapRouter = new PoolSwapTest(manager); + swapRouterNoChecks = new SwapRouterNoChecks(manager); modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); donateRouter = new PoolDonateTest(manager); takeRouter = new PoolTakeTest(manager); @@ -122,8 +125,9 @@ contract Deployers { function deployMintAndApproveCurrency() internal returns (Currency currency) { MockERC20 token = deployTokens(1, 2 ** 255)[0]; - address[6] memory toApprove = [ + address[7] memory toApprove = [ address(swapRouter), + address(swapRouterNoChecks), address(modifyLiquidityRouter), address(donateRouter), address(takeRouter),