diff --git a/docs/contracts/v4/guides/hooks/01-your-first-hook.md b/docs/contracts/v4/guides/hooks/01-your-first-hook.md index 3a8591e30..eff63e645 100644 --- a/docs/contracts/v4/guides/hooks/01-your-first-hook.md +++ b/docs/contracts/v4/guides/hooks/01-your-first-hook.md @@ -16,7 +16,6 @@ Let’s start by defining when users will be rewarded with these points: 1. When the user swaps `ETH` into `TOKEN` they will get awarded points equal to how much `ETH` they swapped the token with. 2. When the user adds liquidity, we award them with points equal to the amount of `ETH` they added. -3. [todo] In order to keep track of these points, we’ll mint the `POINTS` token to the user, this has an added benefit for the user to be able to track it in their wallet. @@ -26,17 +25,81 @@ Let’s figure out how our hook will work. From the Uniswap v4 Documentation, there are several hooks available for developers to integrate with. In our use case, we specifically need the ability to read swaps and figure out what amounts they are swapping for and who they are. -[consider adding a callout for Universal Router here] - For our hook, we’ll be using `afterSwap` and `afterAddLiquidity` hooks. Why these instead of the `before...` hooks? We’ll dig deeper into this later in this guide. +_Note: To initiate the swap in the first place, this is where [`UniversalRouter`](../../../../contracts/universal-router/01-overview.md) comes into play where we will pass in the [`V4_SWAP`](https://github.com/Uniswap/universal-router/blob/main/contracts/libraries/Commands.sol#L35) command to `UniversalRouter.execute`._ + # Let’s create our hook! We’ll call this hook `PointsHook` and create it in such a way that any pool paired with `TOKEN` can use it. ## Setting things up -[todo: base this on the new template repo] +The Uniswap [v4-template repo](https://github.com/uniswapfoundation/v4-template) provides a basic foundry environment with required imports already pre-loaded. Click on [`Use this template`](https://github.com/new?template_name=v4-template&template_owner=uniswapfoundation) to create a new repository with it. + +Or simply clone it and install the dependencies: + +```bash +git clone https://github.com/uniswapfoundation/v4-template.git +cd v4-template +# requires foundry +forge install +forge test +``` + +After that let's create a new contract `PointsHook.sol` in `src` folder with the following codes: + +```solidity +// 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 PointsHook is BaseHook { + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() + public + pure + override + returns (Hooks.Permissions memory) + { + return + Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: true, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } +} +``` + +The above code does the following: + +- import the relevant dependencies +- initialize the constructor by passing in the instance of PoolManager +- override `getHookPermissions` from `BaseHook.sol` to return a struct of permissions to signal which hook functions are to be implemented. + It will also be used at deployment to validate the address correctly represents the expected permissions. + +Awesome! Now it's all set to start building the hook! ## Basic Structure @@ -94,13 +157,13 @@ contract PointsHook is BaseHook { } ``` -You’ll notice that both hooks return their own selector in the functions, this is pattern used by the protocol to signal “successful” invocation. We’ll talk about rest of the return parameters when we start adding the functionality. +You’ll notice that both hooks return their own selector in the functions, this is a pattern used by the protocol to signal “successful” invocation. We’ll talk about rest of the return parameters when we start adding the functionality. Most of the code at this point should be self-explanatory. It’s not doing anything yet, but it’s a great place to start adding the functionality we need. ## Points Logic -Up until here, the hook isn’t actually doing anything, so let’s add some functionality! First, let’s setup the `POINTS` token that we’ll reward people with. +First, let’s setup the `POINTS` token that we’ll reward users with via creating another contract `PointsToken.sol` and import relevant dependencies like `ERC20` and `Owned`. ```solidity contract PointsToken is ERC20, Owned { @@ -121,7 +184,7 @@ contract PointsHook is BaseHook { constructor(IPoolManager _poolManager) BaseHook(_poolManager) { pointsToken = new PointsToken(); } - + [...] } ``` @@ -242,15 +305,9 @@ Similar to what we did for the `afterSwap` hook, now we need to award users for We’re using Foundry for building our hook, and we’ll continue using it to write our tests. One of the great things about Foundry is that you can write tests in Solidity itself instead of context switching between another language. -### Hook Contract Address - -The `PositionManager` for Uniswap v4 expects the hook address to indicate supported flags. - -[todo: this section is completely wrong on UHI atm and needs to be rewritten, should consider moving it out of here and have a singular place explaining hook bits] - ### Test Suite -The starter repo you cloned already has an existing base test file, let’s start by copying it into `PointsHook.t.sol`. +The v4-template repo you cloned already has an existing base test file, let’s start by copying it into `PointsHook.t.sol`. ```solidity contract PointsHookTest is Test, Fixtures { @@ -377,7 +434,6 @@ function test_PointsHook_Liquidity() public { liqToAdd ); - // Let's swap some ETH for the token. posm.mint( key, tickLower, @@ -399,6 +455,6 @@ function test_PointsHook_Liquidity() public { This test case looks very similar to the `afterSwap` one, except we’re testing based on the liquidity added. You’ll notice at the end we’re testing for approximate equality within 10 points. This is to account for minor differences in actual liquidity added due to ticks and pricing. -# What’s next? +# Next Steps -[todo: explain docs and introduce advance concepts] \ No newline at end of file +Congratulations on building your very first hook! You could explore further by going to [Hook Deployment](./05-hook-deployment.mdx) to learn about how hook flags work and see how we will deploy a hook in action. diff --git a/docs/contracts/v4/guides/hooks/04-Volatility-fee-hook.mdx b/docs/contracts/v4/guides/hooks/04-Volatility-fee-hook.mdx deleted file mode 100644 index d48c7abfa..000000000 --- a/docs/contracts/v4/guides/hooks/04-Volatility-fee-hook.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Volatility-Based Dynamic Fee Hook ---- - -This example demonstrates a complete implementation of a volatility-based dynamic fee hook for Uniswap v4, incorporating all key components and functions. - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import {BaseHook} from "@uniswap/v4-core/contracts/BaseHook.sol"; -import {Hooks} from "@uniswap/v4-core/contracts/libraries/Hooks.sol"; -import {IPoolManager} from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; -import {PoolKey} from "@uniswap/v4-core/contracts/types/PoolKey.sol"; -import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/contracts/types/PoolId.sol"; -import {LPFeeLibrary} from "@uniswap/v4-core/contracts/libraries/LPFeeLibrary.sol"; -import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "@uniswap/v4-core/contracts/types/BeforeSwapDelta.sol"; - -interface IVolatilityOracle { - function realizedVolatility() external view returns (uint256); - function latestTimestamp() external view returns (uint256); -} - -contract VolatilityBasedFeeHook is BaseHook { - using PoolIdLibrary for PoolKey; - - uint256 public constant HIGH_VOLATILITY_TRIGGER = 1400; // 14% - uint256 public constant MEDIUM_VOLATILITY_TRIGGER = 1000; // 10% - uint24 public constant HIGH_VOLATILITY_FEE = 10000; // 1% - uint24 public constant MEDIUM_VOLATILITY_FEE = 3000; // 0.3% - uint24 public constant LOW_VOLATILITY_FEE = 500; // 0.05% - - IVolatilityOracle public immutable volatilityOracle; - uint256 public lastFeeUpdate; - uint256 public constant FEE_UPDATE_INTERVAL = 1 hours; - - constructor(IPoolManager _poolManager, IVolatilityOracle _volatilityOracle) BaseHook(_poolManager) { - volatilityOracle = _volatilityOracle; - } - - function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: true, - beforeModifyLiquidity: false, - afterModifyLiquidity: false, - beforeSwap: true, - afterSwap: false, - beforeDonate: false, - afterDonate: false, - noOp: false - }); - } - - function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - returns (bytes4) - { - uint24 initialFee = getFee(); - poolManager.updateDynamicLPFee(key, initialFee); - return IHooks.afterInitialize.selector; - } - - function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - returns (bytes4, BeforeSwapDelta, uint24) - { - if (block.timestamp >= lastFeeUpdate + FEE_UPDATE_INTERVAL) { - uint24 newFee = getFee(); - lastFeeUpdate = block.timestamp; - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, newFee | LPFeeLibrary.OVERRIDE_FEE_FLAG); - } - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); - } - - function getFee(address, PoolKey calldata) public view returns (uint24) { - uint256 realizedVolatility = volatilityOracle.realizedVolatility(); - if (realizedVolatility > HIGH_VOLATILITY_TRIGGER) { - return HIGH_VOLATILITY_FEE; - } else if (realizedVolatility > MEDIUM_VOLATILITY_TRIGGER) { - return MEDIUM_VOLATILITY_FEE; - } else { - return LOW_VOLATILITY_FEE; - } - } -} -``` - -# Volatility-Based Fee Structure - -This hook contract example sets up the structure for a volatility-based fee system, defining thresholds and corresponding fees: - -```solidity -contract VolatilityBasedFeeHook is BaseHook { - uint256 public constant HIGH_VOLATILITY_TRIGGER = 1400; // 14% - uint256 public constant MEDIUM_VOLATILITY_TRIGGER = 1000; // 10% - uint24 public constant HIGH_VOLATILITY_FEE = 10000; // 1% - uint24 public constant MEDIUM_VOLATILITY_FEE = 3000; // 0.3% - uint24 public constant LOW_VOLATILITY_FEE = 500; // 0.05% - - IVolatilityOracle public immutable volatilityOracle; - - constructor(IPoolManager _poolManager, IVolatilityOracle _volatilityOracle) BaseHook(_poolManager) { - volatilityOracle = _volatilityOracle; - } - - // Implementation of getFee and other functions... -} -``` - -where: - -- High volatility tier: > 14% (fee: 1%) -- Medium volatility tier: 10%-14% (fee: 0.30%) -- Low volatility tier: < 10% (fee: 0.05%) - -The constructor sets up the initial parameters and connections to required contracts (PoolManager and VolatilityOracle). - -# Realized Volatility Oracle - -The contract utilizes an oracle to provide historical data on price movements for informed fee adjustments. This could be implemented as an external service or an on-chain mechanism tracking recent price changes. - -```solidity -interface IVolatilityOracle { - function realizedVolatility() external view returns (uint256); - function latestTimestamp() external view returns (uint256); -} -``` - -# getFee Function Implementation - -The getFee function calculates the fee based on the current volatility level and returns the appropriate fee rate. This function implements the logic for dynamically calculating fees based on current conditions. The getFee function should return a fee value based on your chosen criteria (e.g., volatility, volume, etc.). - -```solidity -function getFee(address, PoolKey calldata) external view returns (uint24) { - uint256 realizedVolatility = volatilityOracle.realizedVolatility(); - if (realizedVolatility > HIGH_VOLATILITY_TRIGGER) { - return HIGH_VOLATILITY_FEE; - } else if (realizedVolatility > MEDIUM_VOLATILITY_TRIGGER) { - return MEDIUM_VOLATILITY_FEE; - } else { - return LOW_VOLATILITY_FEE; - } -} -``` - -# beforeSwap Hook Callback - -The `beforeSwap` hook is used to update fees before each swap, ensuring they reflect the most recent market conditions: - -```solidity -function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) - external - override - returns (bytes4, BeforeSwapDelta, uint24) -{ - if (block.timestamp >= lastFeeUpdate + FEE_UPDATE_INTERVAL) { - uint24 newFee = getFee(); - lastFeeUpdate = block.timestamp; - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, newFee | LPFeeLibrary.OVERRIDE_FEE_FLAG); - } - return (IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); -} -``` - -This implementation calculates the new fee and returns it with the `OVERRIDE_FEE_FLAG`, allowing for per-swap fee updates without calling `updateDynamicLPFee`. - -# afterInitialize Hook Callback - -The `afterInitialize` hook is used to set the initial fee for the dynamic fee pool: - -```solidity -function afterInitialize(address, PoolKey calldata key, uint160, int24, bytes calldata) - external - override - returns (bytes4) -{ - uint24 initialFee = getFee(); - poolManager.updateDynamicLPFee(key, initialFee); - return IHooks.afterInitialize.selector; -} -``` - -This ensures that the pool starts with an appropriate fee based on current market conditions. \ No newline at end of file diff --git a/docs/contracts/v4/guides/hooks/05-hook-deployment.mdx b/docs/contracts/v4/guides/hooks/05-hook-deployment.mdx index f513e7346..360396152 100644 --- a/docs/contracts/v4/guides/hooks/05-hook-deployment.mdx +++ b/docs/contracts/v4/guides/hooks/05-hook-deployment.mdx @@ -38,7 +38,7 @@ A full list of all flags can be found [here](https://github.com/Uniswap/v4-core/ ## Hook Miner -Because of encoded addresses, hook developers must _mine_ an address to a their particular pattern +Because of encoded addresses, hook developers must _mine_ an address to a their particular pattern. For local testing, `deployCodeTo` cheatcode in Foundry can be used to deploy hook contract to any address.