Set Up Local Environment
Before writing the hook let's first have a local environment properly configured e.g. deploying pool manager, utility routers and test tokens.
At the end of this guide a development environment will be set up to be used to build the rest of the examples in the Guides section of the docs.
To get started as quickly as possible for building Uniswap v4 hooks, there is a Quick Start
section below to clone a boilerplate and get building. To start from scratch and learn the underlying concepts, jump to the Start from Scratch
section.
Quick Start
The Uniswap v4-template repo provides a basic foundry environment with required imports already pre-loaded. Click on Use this template
to create a new repository with it.
Or simply clone it and install the dependencies:
git clone https://github.com/uniswapfoundation/v4-template.git
cd v4-template
# requires foundry
forge install
forge test
Then hop to the Local Node via Anvil to complete the set up and start developing.
Start from Scratch
In the following sections, let's walk through the steps to create the same environment set up as the boilerplate from scratch and learn the underlying concepts.
Setting up Foundryβ
First thing is to set up a new Foundry project.
If there is no Foundry installed - follow the Foundry Book for installation.
Once Foundry is setup, initialize a new project:
forge init counter-hook
cd counter-hook
Then install the Uniswap v4-core
and v4-periphery
contracts as dependencies:
forge install Uniswap/v4-core && forge install Uniswap/v4-periphery
Next, set up the remappings so that the shorthand syntax for importing contracts from the dependencies work nicely:
forge remappings > remappings.txt
If there is something wrong with the inferred remappings, please replace with the following in
remappings.txt
:
@uniswap/v4-core/=lib/v4-core/
forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/
forge-std/=lib/v4-core/lib/forge-std/src/
permit2/=lib/v4-periphery/lib/permit2/
solmate/=lib/v4-core/lib/solmate/
v4-core/=lib/v4-core/
v4-periphery/=lib/v4-periphery/
After that, remove the default Counter contract and its associated test and script file that Foundry initially set up. To do that, either manually delete those files, or just run the following:
rm ./**/Counter*.sol
Finally, since v4 uses transient storage which is only available after Ethereum's cancun hard fork and on Solidity versions >= 0.8.24 - some config must be set in foundry.toml
config file.
To do that, add the following lines to foundry.toml
:
# foundry.toml
solc_version = "0.8.26"
evm_version = "cancun"
ffi = true
Awesome! Now it's all set to start building the hook! Letβs run a quick test to confirm everything is set up properly.
Compile a Basic Hook Contractβ
To confirm that the environment is configured correctly let's write a basic Counter Hook contract. Create a new file, ./src/CounterHook.sol
and paste the following code into it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol";
contract CounterHook is BaseHook {
using PoolIdLibrary for PoolKey;
// NOTE: ---------------------------------------------------------
// state variables should typically be unique to a pool
// a single hook contract should be able to service multiple pools
// ---------------------------------------------------------------
mapping(PoolId => uint256 count) public beforeSwapCount;
mapping(PoolId => uint256 count) public afterSwapCount;
mapping(PoolId => uint256 count) public beforeAddLiquidityCount;
mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount;
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: true,
afterAddLiquidity: false,
beforeRemoveLiquidity: true,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: true,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
// -----------------------------------------------
// NOTE: see IHooks.sol for function documentation
// -----------------------------------------------
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata)
external
override
returns (bytes4, BeforeSwapDelta, uint24)
{
beforeSwapCount[key.toId()]++;
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}
function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata)
external
override
returns (bytes4, int128)
{
afterSwapCount[key.toId()]++;
return (BaseHook.afterSwap.selector, 0);
}
function beforeAddLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeAddLiquidityCount[key.toId()]++;
return BaseHook.beforeAddLiquidity.selector;
}
function beforeRemoveLiquidity(
address,
PoolKey calldata key,
IPoolManager.ModifyLiquidityParams calldata,
bytes calldata
) external override returns (bytes4) {
beforeRemoveLiquidityCount[key.toId()]++;
return BaseHook.beforeRemoveLiquidity.selector;
}
}
To compile the Counter Hook contracts in the ./src
folder, use the foundry build command:
forge build
If the environment is compiled correctly it will display a message:
Compiler run successful!
Local Node via Anvilβ
Other than writing unit tests, Anvil can be used to deploy and test hooks.
With the local node up and running, use the --rpc-url 127.0.0.1:8545
flag in tests to point the Foundry testing suite to that local node:
# start anvil, a local EVM chain
anvil
# in a new terminal
# foundry script for deploying v4 & hooks to anvil
forge script script/Anvil.s.sol \
--rpc-url http://localhost:8545 \
--private-key <test_wallet_private_key> \
--broadcast
# test on the anvil local node
forge test --rpc-url 127.0.0.1:8545
Next Steps
With the environment set up ready to be built on. Jump over to the guides section to learn about the Uniswap functions that can be integrated with Hook. Remember to add all contracts (.sol files) to the ./src
folder and their subsequent tests to the ./test
folder. Then test them against the local anvil node by running:
forge test --rpc-url 127.0.0.1:8545