Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 10 additions & 56 deletions script/Counter.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,82 +9,36 @@ 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 {}

function run() public {
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));
}
}
}
}
23 changes: 10 additions & 13 deletions test/Counter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,28 @@ 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;

function setUp() public {
// 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));
Expand Down
17 changes: 0 additions & 17 deletions test/implementation/CounterImplementation.sol

This file was deleted.

52 changes: 52 additions & 0 deletions test/utils/HookDeployer.sol
Original file line number Diff line number Diff line change
@@ -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)))))
);
}
}
12 changes: 0 additions & 12 deletions test/utils/HookTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down