Uniswap v4 adds several key updates to improve gas efficiency, customizability, and functionality.
So let's not lose time and dive into it!

By now you've probably heard of Uniswap and so-called AMMs (automated market makers). But if you're not familiar with Uniswap yet, it's a fully decentralized protocol for automated liquidity provision on Ethereum. An easier-to-understand description would be that it's a decentralized exchange (DEX) relying on external liquidity providers that can add tokens to smart contract pools and users can trade those directly.
Since it's running on Ethereum, what we can trade are Ethereum ERC-20 tokens and ETH. Originally for each token there was its own smart contract and liquidity pool, now in Uniswap 4 there is one smart contract that manages the state for all pools. A pool is any two tokens with some customizations for what fees and hooks. We'll discuss this later in more detail.
Uniswap - being fully decentralized - has no restrictions to which tokens can be added. If no pools for a token pair + customization exist yet, anyone can create one and anyone can provide liquidity to a pool.
The price of a token is determined by the liquidity in a pool. For example if a user is buying TOKEN1 with TOKEN2, the supply of TOKEN1 in the pool will decrease while the supply of TOKEN2 will increase and the price of TOKEN1 will increase. Likewise, if a user is selling TOKEN1, the price of TOKEN1 will decrease. Therefore the token price always reflects the supply and demand. This behavior can be described using the known formula: x * y = k.
And of course a user doesn't have to be a person, it can be a smart contract. That allows us to add Uniswap to our own contracts for adding additional payment options for users of our contracts. Uniswap makes this process very convenient, see below for how to integrate it.


What is new in UniSwap v4?
We've discussed what's new in Uniswap v3 here, but now let's see what's new in Uniswap v4:
Hooks: At the heart of Uniswap v4 is a new concept known as 'hooks'. Think of hooks like plugins you can add to your music software to create a new sound effect. Similarly, these hooks can be used to add new functionalities or features to the liquidity pools in Uniswap v4. In practical terms, hooks can enable a variety of functions like setting up limit orders, dynamic fees or creating custom oracle implementations. We'll take a closer look at this feature!
Singleton and Flash Accounting: In previous versions, every new token pair had its own contract. However, Uniswap v4 introduces the singleton design pattern, meaning all pools are managed by a single contract. And Uniswap v4 uses a system called 'flash accounting'. This method only transfers tokens externally at the end of a transaction, updating an internal balance throughout the process. All this improves gas costs a lot.
Native ETH: Uniswap v4 is bringing back support for native ETH. So, instead of wrapping your ETH into an ERC-20 token for trading, you can trade directly using ETH. Another feature for saving gas.
ERC1155 Accounting: With Uniswap v4, you can keep your tokens within the singleton (that mega contract we talked about earlier) and avoid constant transfers to and from the contract. The accounting itself uses the ERC1155 standard which is a multi-token standard. It allows you to send multiple different token classes in one transaction. We've discussed the standard here before.
Governance Updates: Uniswap v4 also brings changes to how fees are managed. There are two separate governance fee mechanisms - swap fees and withdrawal fees. The governing body can take a certain percentage of these fees.
Donate Function: Uniswap v4 introduces a
donatefunction that allows users to pay liquidity providers directly in the tokens of the pool.
Released-v4 correction: The internal token claims use ERC-6909, not ERC-1155. Current swap protocol fees and hook-defined accounting differ from the prototype’s withdrawal-fee interfaces. Flash accounting requires all deltas to settle before the unlock callback returns; it does not require every transfer to wait until one final instant.

Further Resources
What happens to Uniswap v3?

Integrating UniSwap v4
Doing a swap within your contracts is now a little more complex in Uniswap 4. So let's take a look at one example:
The snippets below are pinned to the early prototype commit 86b3f657. Do not replace that pin with a moving branch and expect compatibility. The released API has changed substantially; use the current hook guide for a new project.
Let's investigate the swapTokens function that takes in three parameters:
IPoolManager.PoolKey poolKeyIPoolManager.SwapParams calldata swapParamsuint256 deadline
The poolKey is the identifier for which pool you want to use for the swap. This not only consists of the two token addresses, but also of the specified fees, tick spacing and hooks:
struct PoolKey {
Currency currency0;
Currency currency1;
uint24 fee;
int24 tickSpacing;
IHooks hooks;
}That means there will be an infinite amount of pools per token pair now! It will be up to you to determine which ones to use.
The currency here is actually using the new Solidity feature of custom types:
type Currency is address;So in other words Currency is just an address type. Typically an ERC20 token address. So why not IERC20? Because Currency can also mean native ETH. In this case you have to pass address(0).
As for the IHooks, we'll discuss this in greater details later.
Now the second parameter we send is SwapParams which consists of:
- zeroForOne: A boolean indicating the direction (buy vs. sell)
- amountSpecified: The actual amount you want to swap.
- sqrtPriceLimitX96: This represents the lowest price you are fine to accept. It is a direction-dependent limit on the square root of the token1/token0 price ratio, not the square root of
x * y. And X96 refers to this being represented as a fixed-point decimal with 96 bits of precision to the right of the decimal point.
struct SwapParams {
bool zeroForOne;
int256 amountSpecified;
uint160 sqrtPriceLimitX96;
}Now we look at the flow from swapTokens next, here's where some magic happens. swapTokens calls the lock function on the poolManager. By calling lock, you're asking Uniswap to allow trading against it.
This allows Uniswap to allow you to get negative balances. Why?
As soon as the lock is called, the poolManager contract from Uniswap automatically calls lockAcquired on your contract again. Inside here we can do all the trades and interaction we want. After the call to lockAcquired is finished (so your code is done executing), Uniswap does one last check still:
These names describe the prototype. Released v4 uses unlock and unlockCallback. A callback must authenticate the PoolManager, preserve the original payer/recipient explicitly, use current delta signs and follow the current sync/settle rules. A name substitution alone is insufficient.
if (lockState.nonzeroDeltaCount != 0) {
revert CurrencyNotSettled();
}So at the end of it all, you must have settled your balances again. This keeps token transfers to an absolute minimum, because we have to do it only once at the end.
The function _settleCurrencyBalance is called for each currency involved in the swap. If we have a negative delta it means the pool still owes us money. We can take it back by calling take.
In the other case we have to pay back the amount using settle:
- For ETH we can just send the amount directly along with the
settlecall. - For ERC20 tokens we have to transfer them to the pool manager and then call
settleseparately.
// Prototype swap wrapper withdrawn in the 2026 review.
// Callback authorization was inverted, and callback msg.sender
// was incorrectly used as the original payer/recipient.
// Current v4 integration requires the released unlock lifecycle.
// Start with the maintained guide and verify the exact pinned release:
// https://developers.uniswap.org/docs/protocols/v4/guides/hooks/your-first-hook
The prototype wrapper above is withdrawn. The surrounding explanation records how the early design worked; it is not a tested swap integration.
// Prototype swap wrapper withdrawn in the 2026 review.
// Callback authorization was inverted, and callback msg.sender
// was incorrectly used as the original payer/recipient.
// Current v4 integration requires the released unlock lifecycle.
// Start with the maintained guide and verify the exact pinned release:
// https://developers.uniswap.org/docs/protocols/v4/guides/hooks/your-first-hook
An alternative architecture to the above one that's slightly more flexible could also be this one on the left.
Here we're basically just passing any data from the caller to the lock which is then passed to the lockAcquired. From there we simply call address(this).call(data).
In our case the data then to execute swapTokens should be the swapTokens function signature combined with its input parameters.

Entering a New Uniswap Era with Hooks
Now let's discuss the biggest new feature, the hooks. There are three different hook interfaces available.
The first and biggest one is IHooks:
interface IHooks {
function beforeInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96) external returns (bytes4);
function afterInitialize(address sender, PoolKey calldata key, uint160 sqrtPriceX96, int24 tick) external returns (bytes4);
function beforeModifyPosition(address sender, PoolKey calldata key, ModifyPositionParams calldata params) external returns (bytes4);
function afterModifyPosition(address s, PoolKey calldata k, ModifyPositionParams calldata p, BalanceDelta d) external returns (bytes4);
function beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params) external returns (bytes4);
function afterSwap(address sender, PoolKey calldata key, SwapParams calldata params, BalanceDelta delta) external returns (bytes4);
function beforeDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1) external returns (bytes4);
function afterDonate(address sender, PoolKey calldata key, uint256 amount0, uint256 amount1) external returns (bytes4);
}The IHooks interface in Uniswap 4 provides you with an option to interact with different stages of transactions in your liquidity pools. You can consider hooks as operations that get triggered before and after key operations in your pool.
beforeInitializeandafterInitialize: When a new pool is initialized, e.g. if you want to add some additional setup logic when creating a new pool, this is the place.beforeModifyPositionandafterModifyPosition: When an LP position in a pool is being changed, in other words every time an LP adds/removes liquidity or changes parameters of his LP position.beforeSwapandafterSwap: Before and after a swap operation happens. You can see the flow on the right.beforeDonateandafterDonate: When liquidity is being added to the pool via the newdonatefunction.
Remember, you don't need to implement all the hooks. Depending on your requirements, you can choose to implement only those that you need.
We'll explore how you can add these hooks to a pool soon, but first let's take a look at two other hook interfaces.

interface IHookFeeManager {
function getHookSwapFee(
IPoolManager.PoolKey calldata key
) external view returns (uint8);
function getHookWithdrawFee(
IPoolManager.PoolKey calldata key
) external view returns (uint8);
}The IHookFeeManager is a separate interface from the IHooks related to allowing a hook itself to take some fee.
getHookSwapFee: This method allows a hook to define how much cut a hook should get when a swap happens.getHookWithdrawFee: This method sets how much a hook can charge when assets are withdrawn from the pool.
IHookFeeManager is part of the prototype shown here. It is not the released v4 hook-fee API.
Lastly the IDynamicFeeManager interface is used for determining the regular swap fees:
getFee: This method returns the dynamic fee for a pool. This allows for more flexible and potentially changing fee structures, e.g. you could base fees on factors like market conditions.
The released dynamic-fee mechanism uses the dynamic LP fee flag and supported hook fee-update/override paths. The prototype IDynamicFeeManager.getFee interface below should not be copied into a current integration.
interface IDynamicFeeManager {
function getFee(
IPoolManager.PoolKey calldata key
) external returns (uint24);
}Now those hooks in total are available.
But how do you now create a pool with custom hooks?
Creating Custom Uniswap 4 Hooks

Well the short answer is, you can use this new template I created for foundry as a starting point:
This repository is the article’s historical prototype template. For a new project, follow the maintained release-API guide and pin compatible dependencies.
$ forge init my-project --template https://github.com/soliditylabs/uniswap-v4-custom-poolAnd check out the repository over at https://github.com/soliditylabs/uniswap-v4-custom-pool.
But now to the longer answer with some explanations. There are two different mechanisms for how the fees are executed:
- The prefix of the deployed Ethereum address itself of your Hooks contract.
- Special flags in the fee definition, if set to 1, will trigger the specific fee hook.
Let's start with the first one. There's a bit of magic in here, but it's not too hard.
IHooks has 8 defined hook functions. Each function corresponds to a bit in the beginning of your Ethereum address.
What do we mean with that?
Imagine you have a random address like 0x480f0d4887ed4f16d2299031dffec90782826269. The first two characters `0x48` represented in binary will be 01001000.
This will result in the hooks afterInitialize and beforeSwap being executed.
If you want to run all hooks for example, you must make sure that your deployed address starts with `0xff`.
Now the next question is, how do you ensure that the address starts with the correct bits?
Usually the address of a deployed contract will be a combination of the deployer address, its transaction nonce and the deployed bytecode. That would make deployments quite challenging.
Fortunately Ethereum has a second way to deploy contracts using the CREATE2 opcode.
Current flag layout: Released v4 uses the low 14 bits of the hook address, not the first byte. The 0xff prefix examples below belong only to the prototype. Normal CREATE addresses depend on the creator and its nonce; deployed bytecode is not part of that CREATE address formula.

Deploying Hook at the Correct Address
contract UniswapHooksFactory {
function deploy(
address owner,
IPoolManager poolManager,
bytes32 salt
) external returns (address) {
return address(new MyHooksContract{salt: salt}(owner, poolManager));
}
function getPrecomputedHookAddress(
address owner,
IPoolManager pm,
bytes32 salt
) external view returns (address) {
bytes32 bytecodeHash = keccak256(abi.encodePacked(
type(MyHooksContract).creationCode, abi.encode(owner, pm)
));
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xff), address(this), salt, bytecodeHash)
);
return address(uint160(uint256(hash)));
}
}CREATE2 deployments take in a salt parameter. In Solidity this can be done by simply adding {salt: salt} to the deployment call.
The CREATE2 address depends on the deploying contract address, the salt and the hash of the exact initcode, including constructor arguments. It is independent of that deploying contract’s nonce.
We can then also easily precompute what the address of such a Hook would be. Note that the 0xff here is a different concept and has nothing to do with the hooks. You can read the CREATE2 EIP here, to learn more on this.
Given this easy way to determine the address, to deploy our hooks we only need to find the correct salt that gives us the address we want. This is a small brute-force effort.
If you're using Foundry, you can add something like this to your deploy script:
for (uint256 i = 0; i < 1500; i++) {
bytes32 salt = bytes32(i);
address expectedAddress = uniswapHooksFactory.getPrecomputedHookAddress(owner, poolManager, salt);
if (_doesAddressStartWith(expectedAddress, 0xff)) {
IHooks(uniswapHooksFactory.deploy(owner, poolManager, salt));
}
}
function _doesAddressStartWith(address _address,uint160 _prefix) private pure returns (bool) {
return uint160(_address) / (2 ** (8 * (19))) == _prefix;
}The fixed 1,500-attempt loop is only a prototype illustration and does not guarantee a matching salt. A deployment script must confirm it found the intended permission bits and validate the result before deployment; the current bit layout differs from the prefix used here.
import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol";
constructor(IPoolManager _poolManager) {
poolManager = _poolManager;
validateHookAddress(this);
}
function validateHookAddress(BaseHook _this) internal pure {
Hooks.validateHookAddress(_this, getHooksCalls());
}
function getHooksCalls() public pure returns (Hooks.Calls memory) {
return Hooks.Calls({
beforeInitialize: true,
afterInitialize: true,
beforeModifyPosition: true,
afterModifyPosition: true,
beforeSwap: true,
afterSwap: true,
beforeDonate: true,
afterDonate: true
});
}Now as a last way to verify you have set the address correctly, we can use the Hooks.validateHookAddress function provided by Uniswap.
Inside the constructor of our Hook we can call validateHookAddress with the booleans set exactly to those hooks we want to have registered.
If the address doesn't match this specification, the deployment will fail.
For example on the left it would only succeed if the address actually starts with `0xff`, because we have set all hooks to be required.
Setting Fee Hooks
Now the mechanism for the fee hooks is slightly different.
When you initialize a pool in the poolManager, you have to call something like on the right. If you want the fee hooks to be executed, you have to set the specific flags here for myFees, if not you would put a static fee number here.
poolManager.initialize(IPoolManager.PoolKey({
currency0: Currency.wrap(address(token1)),
currency1: Currency.wrap(address(token2)),
fee: myFees,
tickSpacing: 1,
hooks: IHooks(deployedHooks)
}), sqrtPriceX96);For example if you want all fee hooks to be executed, you would just set all flags by setting myFees like this:
uint24 myFees = Fees.DYNAMIC_FEE_FLAG + Fees.HOOK_SWAP_FEE_FLAG + Fees.HOOK_WITHDRAW_FEE_FLAG;That's it! You can dive into the full examples over at https://github.com/soliditylabs/uniswap-v4-custom-pool.
And use them as a starting point in Foundry like this:
$ forge init my-project --template https://github.com/soliditylabs/uniswap-v4-custom-poolAnd also worth noting, you can find some examples for implemented hooks here: https://github.com/Uniswap/v4-periphery/tree/main/contracts/hooks/examples.
One interesting example is the feature to enable limit orders. On a high level it works as follows. The hook contract has two functions:
function placeLimitOrder(PoolKey calldata key, int24 tickLower, bool zeroForOne, uint128 liquidity): Allow users to add liquidity to the hook at specified ticks.function afterSwap: Inside the hook, we can check the new ticks and modify the hook's LP position accordingly.
That's it. You've reached the end. Now good luck on your Uniswap journey.
Onwards and upwards.




Join the conversation
Comments are hosted by Disqus and load only when you choose to enable them.