diff --git a/script/Counter.s.sol b/script/Counter.s.sol index 64ba9948..e11731ad 100644 --- a/script/Counter.s.sol +++ b/script/Counter.s.sol @@ -9,11 +9,10 @@ import {PoolModifyPositionTest} from "@uniswap/v4-core/contracts/test/PoolModify import {PoolSwapTest} from "@uniswap/v4-core/contracts/test/PoolSwapTest.sol"; import {PoolDonateTest} from "@uniswap/v4-core/contracts/test/PoolDonateTest.sol"; import {Counter} from "../src/Counter.sol"; -import {CounterImplementation} from "../test/implementation/CounterImplementation.sol"; +import {HookDeployer} from "../test/utils/HookDeployer.sol"; /// @notice Forge script for deploying v4 & hooks to **anvil** /// @dev This script only works on an anvil RPC because v4 exceeds bytecode limits -/// @dev and we also need vm.etch() to deploy the hook to the proper address contract CounterScript is Script { function setUp() public {} @@ -21,70 +20,25 @@ contract CounterScript is Script { vm.broadcast(); PoolManager manager = new PoolManager(500000); - // uniswap hook addresses must have specific flags encoded in the address - // (attach 0x1 to avoid collisions with other hooks) - uint160 targetFlags = uint160( + // hook contracts must have specific flags encoded in the address + uint160 flags = uint160( Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG - | Hooks.AFTER_MODIFY_POSITION_FLAG | 0x1 + | Hooks.AFTER_MODIFY_POSITION_FLAG ); - // TODO: eventually use bytecode to deploy the hook with create2 to mine proper addresses - // bytes memory hookBytecode = abi.encodePacked(type(Counter).creationCode, abi.encode(address(manager))); - // TODO: eventually we'll want to use `uint160 salt` in the return create2 deploy the hook - // (address hook,) = mineSalt(targetFlags, hookBytecode); - // require(uint160(hook) & targetFlags == targetFlags, "CounterScript: could not find hook address"); + // Mine a salt that will produce a hook address with the correct flags + bytes memory hookBytecode = abi.encodePacked(type(Counter).creationCode, abi.encode(address(manager))); + (, uint256 salt) = HookDeployer.mineSalt(flags, hookBytecode); + // Deploy the hook using the CREATE2 Deployer Proxy (provided by anvil) vm.broadcast(); - // until i figure out create2 deploys on an anvil RPC, we'll use the etch cheatcode - CounterImplementation impl = new CounterImplementation(manager, Counter(address(targetFlags))); - etchHook(address(impl), address(targetFlags)); + HookDeployer.deployWithSalt(hookBytecode, salt); + // Additional helpers for interacting with the pool vm.startBroadcast(); - // Helpers for interacting with the pool new PoolModifyPositionTest(IPoolManager(address(manager))); new PoolSwapTest(IPoolManager(address(manager))); new PoolDonateTest(IPoolManager(address(manager))); vm.stopBroadcast(); } - - function mineSalt(uint160 targetFlags, bytes memory creationCode) - internal - view - returns (address hook, uint256 salt) - { - for (salt; salt < 100; salt++) { - hook = _getAddress(salt, creationCode); - if (uint160(hook) & targetFlags == targetFlags) { - break; - } - } - } - - function _getAddress(uint256 salt, bytes memory creationCode) internal view returns (address) { - return address( - uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(creationCode))))) - ); - } - - function etchHook(address _implementation, address _hook) internal { - (, bytes32[] memory writes) = vm.accesses(_implementation); - - // courtesy of horsefacts - // https://github.com/farcasterxyz/contracts/blob/de8aa0723a5c83b5682fd6d3a1123ea5fced179e/script/Deploy.s.sol#L54 - string[] memory command = new string[](5); - command[0] = "cast"; - command[1] = "rpc"; - command[2] = "anvil_setCode"; - command[3] = vm.toString(_hook); - command[4] = vm.toString(_implementation.code); - vm.ffi(command); - - // 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(_hook, slot, vm.load(_implementation, slot)); - } - } - } } diff --git a/test/Counter.t.sol b/test/Counter.t.sol index 42d35387..0b601fe4 100644 --- a/test/Counter.t.sol +++ b/test/Counter.t.sol @@ -13,20 +13,13 @@ import {Deployers} from "@uniswap/v4-core/test/foundry-tests/utils/Deployers.sol import {CurrencyLibrary, Currency} from "@uniswap/v4-core/contracts/types/Currency.sol"; import {HookTest} from "./utils/HookTest.sol"; import {Counter} from "../src/Counter.sol"; -import {CounterImplementation} from "./implementation/CounterImplementation.sol"; +import {HookDeployer} from "./utils/HookDeployer.sol"; contract CounterTest is HookTest, Deployers, GasSnapshot { using PoolIdLibrary for PoolKey; using CurrencyLibrary for Currency; - Counter counter = Counter( - address( - uint160( - Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG - | Hooks.AFTER_MODIFY_POSITION_FLAG - ) - ) - ); + Counter counter; PoolKey poolKey; PoolId poolId; @@ -34,10 +27,14 @@ contract CounterTest is HookTest, Deployers, GasSnapshot { // creates the pool manager, test tokens, and other utility routers HookTest.initHookTestEnv(); - // testing environment requires our contract to override `validateHookAddress` - // well do that via the Implementation contract to avoid deploying the override with the production contract - CounterImplementation impl = new CounterImplementation(manager, counter); - etchHook(address(impl), address(counter)); + // Deploy the hook to an address with the correct flags + uint160 flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_MODIFY_POSITION_FLAG + | Hooks.AFTER_MODIFY_POSITION_FLAG + ); + bytes memory hookBytecode = abi.encodePacked(type(Counter).creationCode, abi.encode(address(manager))); + address hook = HookDeployer.deploy(flags, hookBytecode); + counter = Counter(hook); // Create the pool poolKey = PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(counter)); diff --git a/test/implementation/CounterImplementation.sol b/test/implementation/CounterImplementation.sol deleted file mode 100644 index 5977825a..00000000 --- a/test/implementation/CounterImplementation.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.19; - -import {Counter} from "../../src/Counter.sol"; - -import {BaseHook} from "v4-periphery/BaseHook.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; - -contract CounterImplementation is Counter { - constructor(IPoolManager poolManager, Counter addressToEtch) Counter(poolManager) { - Hooks.validateHookAddress(addressToEtch, getHooksCalls()); - } - - // make this a no-op in testing - function validateHookAddress(BaseHook _this) internal pure override {} -} diff --git a/test/utils/HookDeployer.sol b/test/utils/HookDeployer.sol new file mode 100644 index 00000000..c126f5b5 --- /dev/null +++ b/test/utils/HookDeployer.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title HookDeployer - a library for deploying Uni V4 hooks with target flags +/// @notice - This library assumes Arachnid's deterministic deployment proxy is available at 0x4e59b44847b379578588920cA78FbF26c0B4956C +/// (true for anvil and most testnets) +library HookDeployer { + // Arachnid's deterministic deployment proxy + // provided by anvil, by default + // https://github.com/Arachnid/deterministic-deployment-proxy + address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + uint160 constant UNISWAP_FLAG_MASK = 0xff << 152; + + function deploy(uint160 targetFlags, bytes memory creationCode) external returns (address) { + (, uint256 salt) = mineSalt(targetFlags, creationCode); + return deployWithSalt(creationCode, salt); + } + + function deployWithSalt(bytes memory creationCode, uint256 salt) public returns (address) { + // Deploy the hook using the CREATE2 Deployer Proxy (provided by anvil) + (bool success,) = address(CREATE2_DEPLOYER).call(abi.encodePacked(salt, creationCode)); + require(success, "HookDeployer: could not deploy hook"); + return _getAddress(salt, creationCode); + } + + function mineSalt(uint160 targetFlags, bytes memory creationCode) + internal + pure + returns (address hook, uint256 salt) + { + uint160 prefix = 1; + for (salt; salt < 1000;) { + hook = _getAddress(salt, creationCode); + prefix = uint160(hook) & UNISWAP_FLAG_MASK; + if (prefix == targetFlags) { + break; + } + + unchecked { + ++salt; + } + } + require(uint160(hook) & UNISWAP_FLAG_MASK == targetFlags, "HookDeployer: could not find hook address"); + } + + /// @notice Precompute a contract address that is deployed with the CREATE2Deployer + function _getAddress(uint256 salt, bytes memory creationCode) internal pure returns (address) { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), CREATE2_DEPLOYER, salt, keccak256(creationCode))))) + ); + } +} diff --git a/test/utils/HookTest.sol b/test/utils/HookTest.sol index 15f4f5b0..14da95bb 100644 --- a/test/utils/HookTest.sol +++ b/test/utils/HookTest.sol @@ -56,18 +56,6 @@ contract HookTest is Test { token1.approve(address(swapRouter), amount); } - function etchHook(address _implementation, address _hook) internal { - (, bytes32[] memory writes) = vm.accesses(_implementation); - vm.etch(_hook, _implementation.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(_hook, slot, vm.load(_implementation, slot)); - } - } - } - function swap(PoolKey memory key, int256 amountSpecified, bool zeroForOne) internal { IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ zeroForOne: zeroForOne,