diff --git a/remappings.txt b/remappings.txt index e05c5bd6..7fee2a9a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,3 +2,4 @@ solmate/=lib/solmate/src/ forge-std/=lib/forge-std/src/ @openzeppelin/=lib/openzeppelin-contracts/ +forge-gas-snapshot/=lib/forge-gas-snapshot/src/ diff --git a/test/FullRange.t.sol b/test/FullRange.t.sol index f0867ba4..d6dc02e8 100644 --- a/test/FullRange.t.sol +++ b/test/FullRange.t.sol @@ -5,7 +5,6 @@ import {Test} from "forge-std/Test.sol"; import {GasSnapshot} from "forge-gas-snapshot/GasSnapshot.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; import {FullRange} from "../contracts/hooks/examples/FullRange.sol"; -import {FullRangeImplementation} from "./shared/implementation/FullRangeImplementation.sol"; import {PoolManager} from "@uniswap/v4-core/src/PoolManager.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol"; @@ -20,6 +19,7 @@ import {UniswapV4ERC20} from "../contracts/libraries/UniswapV4ERC20.sol"; import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol"; import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; contract TestFullRange is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -64,9 +64,7 @@ contract TestFullRange is Test, Deployers, GasSnapshot { MockERC20 token1; MockERC20 token2; - FullRangeImplementation fullRange = FullRangeImplementation( - address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG)) - ); + FullRange fullRange; PoolId id; @@ -85,8 +83,9 @@ contract TestFullRange is Test, Deployers, GasSnapshot { token1 = tokens[1]; token2 = tokens[2]; - FullRangeImplementation impl = new FullRangeImplementation(manager, fullRange); - vm.etch(address(fullRange), address(impl).code); + uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG); + (, bytes32 salt) = HookMiner.find(address(this), flags, type(FullRange).creationCode, abi.encode(manager)); + fullRange = new FullRange{salt: salt}(manager); key = createPoolKey(token0, token1); id = key.toId(); diff --git a/test/GeomeanOracle.t.sol b/test/GeomeanOracle.t.sol index 05255e93..109761f7 100644 --- a/test/GeomeanOracle.t.sol +++ b/test/GeomeanOracle.t.sol @@ -16,6 +16,7 @@ import {PoolModifyLiquidityTest} from "@uniswap/v4-core/src/test/PoolModifyLiqui import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {Oracle} from "../contracts/libraries/Oracle.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; contract TestGeomeanOracle is Test, Deployers { using PoolIdLibrary for PoolKey; @@ -24,14 +25,7 @@ contract TestGeomeanOracle is Test, Deployers { TestERC20 token0; TestERC20 token1; - GeomeanOracleImplementation geomeanOracle = GeomeanOracleImplementation( - address( - uint160( - Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG - | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG - ) - ) - ); + GeomeanOracle geomeanOracle; PoolId id; function setUp() public { @@ -41,18 +35,16 @@ contract TestGeomeanOracle is Test, Deployers { token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); - vm.record(); - GeomeanOracleImplementation impl = new GeomeanOracleImplementation(manager, geomeanOracle); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(geomeanOracle), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(geomeanOracle), slot, vm.load(address(impl), slot)); - } - } - geomeanOracle.setTime(1); + manager = new PoolManager(500000); + + uint160 flags = uint160( + Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG + ); + (, bytes32 salt) = HookMiner.find(address(this), flags, type(GeomeanOracle).creationCode, abi.encode(manager)); + geomeanOracle = new GeomeanOracle{salt: salt}(manager); + + vm.warp(1); key = PoolKey(currency0, currency1, 0, MAX_TICK_SPACING, geomeanOracle); id = key.toId(); @@ -139,7 +131,7 @@ contract TestGeomeanOracle is Test, Deployers { function testBeforeModifyPositionObservation() public { manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds + skip(2); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( @@ -162,7 +154,7 @@ contract TestGeomeanOracle is Test, Deployers { function testBeforeModifyPositionObservationAndCardinality() public { manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds + skip(2); // advance 2 seconds geomeanOracle.increaseCardinalityNext(key, 2); GeomeanOracle.ObservationState memory observationState = geomeanOracle.getState(key); assertEq(observationState.index, 0); @@ -200,7 +192,7 @@ contract TestGeomeanOracle is Test, Deployers { function testPermanentLiquidity() public { manager.initialize(key, SQRT_RATIO_2_1, ZERO_BYTES); - geomeanOracle.setTime(3); // advance 2 seconds + skip(2); // advance 2 seconds modifyLiquidityRouter.modifyLiquidity( key, IPoolManager.ModifyLiquidityParams( diff --git a/test/LimitOrder.t.sol b/test/LimitOrder.t.sol index 9b9e3116..7b8ed791 100644 --- a/test/LimitOrder.t.sol +++ b/test/LimitOrder.t.sol @@ -15,6 +15,7 @@ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol"; import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; import {HookEnabledSwapRouter} from "./utils/HookEnabledSwapRouter.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; contract TestLimitOrder is Test, Deployers { using PoolIdLibrary for PoolKey; @@ -24,7 +25,7 @@ contract TestLimitOrder is Test, Deployers { HookEnabledSwapRouter router; TestERC20 token0; TestERC20 token1; - LimitOrder limitOrder = LimitOrder(address(uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_SWAP_FLAG))); + LimitOrder limitOrder; PoolId id; function setUp() public { @@ -35,17 +36,9 @@ contract TestLimitOrder is Test, Deployers { token0 = TestERC20(Currency.unwrap(currency0)); token1 = TestERC20(Currency.unwrap(currency1)); - vm.record(); - LimitOrderImplementation impl = new LimitOrderImplementation(manager, limitOrder); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(limitOrder), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(limitOrder), slot, vm.load(address(impl), slot)); - } - } + uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.AFTER_SWAP_FLAG); + (, bytes32 salt) = HookMiner.find(address(this), flags, type(LimitOrder).creationCode, abi.encode(manager)); + limitOrder = new LimitOrder{salt: salt}(manager); // key = PoolKey(currency0, currency1, 3000, 60, limitOrder); (key, id) = initPoolAndAddLiquidity(currency0, currency1, limitOrder, 3000, SQRT_RATIO_1_1, ZERO_BYTES); diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 96941963..3880d960 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -20,6 +20,7 @@ import {CurrencyLibrary, Currency} from "@uniswap/v4-core/src/types/Currency.sol import {TWAMM} from "../contracts/hooks/examples/TWAMM.sol"; import {ITWAMM} from "../contracts/interfaces/ITWAMM.sol"; import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {HookMiner} from "./utils/HookMiner.sol"; contract TWAMMTest is Test, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; @@ -43,9 +44,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 earningsFactorLast ); - TWAMM twamm = - TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); - address hookAddress; + TWAMM twamm; MockERC20 token0; MockERC20 token1; PoolKey poolKey; @@ -58,16 +57,12 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { token0 = MockERC20(Currency.unwrap(currency0)); token1 = MockERC20(Currency.unwrap(currency1)); - TWAMMImplementation impl = new TWAMMImplementation(manager, 10_000, twamm); - (, bytes32[] memory writes) = vm.accesses(address(impl)); - vm.etch(address(twamm), address(impl).code); - // for each storage key that was written during the hook implementation, copy the value over - unchecked { - for (uint256 i = 0; i < writes.length; i++) { - bytes32 slot = writes[i]; - vm.store(address(twamm), slot, vm.load(address(impl), slot)); - } - } + // Find a salt that produces a hook address with the desired `flags` + uint160 flags = uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG); + (, bytes32 salt) = HookMiner.find(address(this), flags, type(TWAMM).creationCode, abi.encode(manager, 10_000)); + + // Deploy hook to the precomputed address using the salt + twamm = new TWAMM{salt: salt}(manager, 10_000); (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_RATIO_1_1, ZERO_BYTES); diff --git a/test/utils/HookMiner.sol b/test/utils/HookMiner.sol new file mode 100644 index 00000000..bdeca3fe --- /dev/null +++ b/test/utils/HookMiner.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.21; + +/// @title HookMiner - a library for mining hook addresses +/// @dev This library is intended for `forge test` environments. There may be gotchas when using salts in `forge script` or `forge create` +library HookMiner { + // mask to slice out the top 12 bit of the address + uint160 constant FLAG_MASK = 0xFFF << 148; + + // Maximum number of iterations to find a salt, avoid infinite loops + uint256 constant MAX_LOOP = 20_000; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return hookAddress salt and corresponding address that was found. The salt can be used in `new Hook{salt: salt}()` + function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) + internal + view + returns (address, bytes32) + { + address hookAddress; + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + uint256 salt; + for (salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract + function computeAddress(address deployer, uint256 salt, bytes memory creationCode) + public + pure + returns (address hookAddress) + { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCode))))) + ); + } +}