diff --git a/docs/contracts/permit2/_category_.json b/docs/contracts/permit2/_category_.json index 188a815559..54a6ea7d5b 100644 --- a/docs/contracts/permit2/_category_.json +++ b/docs/contracts/permit2/_category_.json @@ -1,6 +1,5 @@ { "label": "Permit2", - "position": 3, + "position": 4, "collapsed": true } - \ No newline at end of file diff --git a/docs/contracts/uniswapx/_category_.json b/docs/contracts/uniswapx/_category_.json index 181e268a8b..b21c97a10f 100644 --- a/docs/contracts/uniswapx/_category_.json +++ b/docs/contracts/uniswapx/_category_.json @@ -1,6 +1,5 @@ { "label": "UniswapX", - "position": 2, + "position": 3, "collapsed": true } - \ No newline at end of file diff --git a/docs/contracts/universal-router/_category_.json b/docs/contracts/universal-router/_category_.json index 87af9f06ae..2fa3387d11 100644 --- a/docs/contracts/universal-router/_category_.json +++ b/docs/contracts/universal-router/_category_.json @@ -1,6 +1,5 @@ { "label": "Universal Router", - "position": 2, + "position": 3, "collapsed": true } - \ No newline at end of file diff --git a/docs/contracts/v1/_category_.json b/docs/contracts/v1/_category_.json index eec806c1af..de522263ab 100644 --- a/docs/contracts/v1/_category_.json +++ b/docs/contracts/v1/_category_.json @@ -1,5 +1,5 @@ { "label": "V1 Protocol", - "position": 5, + "position": 6, "collapsed": true } diff --git a/docs/contracts/v2/_category_.json b/docs/contracts/v2/_category_.json index 385b509b43..56ca4aba58 100644 --- a/docs/contracts/v2/_category_.json +++ b/docs/contracts/v2/_category_.json @@ -1,5 +1,5 @@ { "label": "V2 Protocol", - "position": 4, + "position": 5, "collapsed": true } diff --git a/docs/contracts/v3/_category_.json b/docs/contracts/v3/_category_.json index 1579e42bdf..74d7182c61 100644 --- a/docs/contracts/v3/_category_.json +++ b/docs/contracts/v3/_category_.json @@ -1,5 +1,5 @@ { "label": "V3 Protocol", - "position": 1, + "position": 2, "collapsed": false -} \ No newline at end of file +} diff --git a/docs/contracts/v4/_category_.json b/docs/contracts/v4/_category_.json new file mode 100644 index 0000000000..93b9edc848 --- /dev/null +++ b/docs/contracts/v4/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "V4 Protocol", + "position": 1, + "collapsed": false +} diff --git a/docs/contracts/v4/concepts/01-intro-to-v4.mdx b/docs/contracts/v4/concepts/01-intro-to-v4.mdx new file mode 100644 index 0000000000..c2a3d1d03f --- /dev/null +++ b/docs/contracts/v4/concepts/01-intro-to-v4.mdx @@ -0,0 +1,43 @@ +--- +id: intro-to-v4 +title: Introduction to Uniswap V4 +sidebar_position: 0.5 +--- + +Uniswap V4, the latest iteration of the Uniswap protocol, is a significant advancement in decentralized +exchanges (DEXs) and automated market makers (AMMs). Here are the key features of Uniswap V4: + +**Customizability with Hooks:** Uniswap V4 introduces a new feature called "hooks", which are essentially smart +contracts that can be attached to liquidity pools. These hooks enable a high degree of customization, allowing +developers to implement specific functionalities at different points in a pool's lifecycle, such as before or after +swaps and liquidity modifications. + +For example, hooks can enable order types (i.e. limit order), specially-tailored oracles, or custom AMM curves. The +flexibility of hooks allows for a broad range of innovations while maintaining the core efficiency of the platform. + +**Singleton Contract for Efficiency:** A significant architectural change in Uniswap V4 is the introduction of a +Singleton contract. In previous versions, each token pair required a separate smart contract, leading to higher gas +costs, especially in multi-hop trades. The Singleton contract model consolidates all pools into a single contract, +significantly reducing gas costs for both trading and pool creation. This model allows for more efficient multi-hop +trades as tokens do not need to be transferred between multiple contracts. Additionally, creating a new pool in +V4 is 99% cheaper in gas costs compared to V3, lowering barriers for setting up new pools. + +**Flash Accounting System:** Another innovative feature in Uniswap V4 is the "flash accounting" system. This system +allows users to efficiently chain together multiple actions in a single transaction, such as swap-and-add-liquidity. +The system tracks the net balances of inbound and outbound tokens; at the end of the transaction, +the contract verifies all debts have been settled. If the user hasn’t settled their debts, the entire transaction reverts, +ensuring security and efficiency. This system is similar to flash loans in concept and is part of the effort to +reduce gas costs and enhance transaction efficiency on the platform. + +**Unlimited Fee Tiers:** Uniswap V4 allows unlimited fee tiers for various liquidity pools. This flexibility allows +for a more tailored approach in catering to a diverse range of assets and trading strategies. Each pool can have its +own unique fee tiers, optimizing the platform's appeal to a wider spectrum of users and market needs. + +**Native ETH Support**: Uniswap V4 enhances user experience by enabling direct trading pairs with native ETH, +eliminating the need for WETH (Wrapped ETH). This simplification streamlines the trading process and lowers transaction +costs. + +**Community-Driven Development and Innovation:** Uniswap V4 emphasizes a community-driven approach to development +and innovation. Since its code release, there has been active community engagement, with many issues, pull requests, +and unique feature ideas contributed by users. The protocol is +designed to encourage innovation, allowing the global community to shape the future of AMMs. diff --git a/docs/contracts/v4/concepts/02-1-overview.mdx b/docs/contracts/v4/concepts/02-1-overview.mdx new file mode 100644 index 0000000000..3957c1f9b7 --- /dev/null +++ b/docs/contracts/v4/concepts/02-1-overview.mdx @@ -0,0 +1,29 @@ +--- +id: v4-architecture-overview +title: V4 Architecture Overview +sidebar_position: 1 +--- + +# Architecture +In Uniswap V3, each pool has its own contract instance, which makes initializing pools and performing swaps in multiple pools costly. +Whereas, in V4, all pools are kept in a single contract to provide substantial gas savings. + +![High Level Architecture](./images/01_Pool_Initialization/v3_high_level_architecture.png) + +![High Level Architecture](./images/01_Pool_Initialization/v4_high_level_architecture.png) + +Early calculations say that V4 will +make the gas cost of creating pools 99% less. Hooks offer unlimited choices and the single contract lets you easily +move through all these choices. + +This Singleton design is improved by a new "flash accounting" method. Instead of moving assets in and out of pools +after each swap in V3, this method only moves the net balances. This means the system is a lot more efficient and +saves even more gas in Uniswap V4. + +![V3 Detailed Architecture](./images/01_Pool_Initialization/v3_detailed_architecture.png) + +![V4 Detailed Architecture](./images/01_Pool_Initialization/v4_detailed_architecture.png) + +Because of the efficiency of the Singleton contract and flash accounting, there is no need to limit fee tiers. People +who create pools can choose them to be most competitive or change them with a dynamic fee hook. V4 also supports +native ETH again, which helps save more gas. diff --git a/docs/contracts/v4/concepts/02-2-pool-manager-and-initialization.mdx b/docs/contracts/v4/concepts/02-2-pool-manager-and-initialization.mdx new file mode 100644 index 0000000000..c714072d7f --- /dev/null +++ b/docs/contracts/v4/concepts/02-2-pool-manager-and-initialization.mdx @@ -0,0 +1,195 @@ +--- +id: pool-manager-and-pool-initialization +title: Pool Manager and Pool Initialization +sidebar_position: 1 +--- + +# PoolManager +To understand the major parts of the PoolManager, let's look at the the main interface: `IPoolManager.sol`. + +https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol + +### Lifecycle Functions + +1. **initialize**: This function initializes a new pool with specified parameters such as pool key, initial price, and +optional hook data, setting up the fundamental characteristics of the pool. + +2. **swap**: Allows users to exchange one type of currency for another within a specified pool, adhering to set limits +and conditions, and adjusting the pool's state accordingly. + +3. **modifyLiquidity**: Enables users to change the amount of liquidity they've provided to a pool, either by adding or +removing it, based on specified upper and lower tick limits. + +### Balance Functions +1. **mint (related to ERC6909 claims)**: Used for creating new claim tokens (as per ERC6909 standards) for a user, +denoting specific rights or entitlements, but not representing liquidity provider (LP) receipt tokens. + +2. **burn (related to ERC6909 claims)**: Allows users to destroy their claim tokens (compliant with ERC6909), +effectively relinquishing the rights or entitlements those tokens represented. + +3. **take**: This function allows users to withdraw or "net out" a specified amount of a currency, which could be seen +as a mechanism for zero-cost flash loans under certain conditions. + +4. **settle**: Used by users to pay off any outstanding amounts they owe, potentially in a different currency, with +the function returning the amount paid. + +The `mint` and `burn` functions are specifically related to handling ERC6909 claims, which are distinct from liquidity +provider receipt tokens. These functions deal with specific claims or entitlements rather than the representation of a +user's share in the liquidity pool. + + +# Pool Initialization +The `initialize` function sets up a new liquidity pool in Uniswap. It takes necessary information such as currencies +and pricing info, and hook information as inputs, checks various conditions to ensure that the pool is set up correctly, +and sets initial values for the pool. + +https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol +```solidity +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.20; + +// ... [other imports and contract definitions] + +/// @notice Holds the state for all pools +contract PoolManager is IPoolManager, Fees, NoDelegateCall, ERC6909Claims { + // ... [other definitions and functions] + + /// @inheritdoc IPoolManager + function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + override + onlyByLocker // Modifier ensures only the current locker can call this function + returns (int24 tick) + { + // Check if the fee specified in the key is too large + if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge(); + + // Validate tick spacing - it must be within defined min and max limits + if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge(); + if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall(); + + // Ensure the currency order is correct (currency0 < currency1) + if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual(); + + // Validate the hooks contract address + if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks)); + + // Call before initialization hook with provided data + key.hooks.beforeInitialize(key, sqrtPriceX96, hookData); + + // Convert the PoolKey to a PoolId + PoolId id = key.toId(); + + // Fetch protocol fee and dynamic swap fee if applicable + (, uint16 protocolFee) = _fetchProtocolFee(key); + uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee(); + + // Initialize the pool with the given parameters and receive the current tick + tick = pools[id].initialize(sqrtPriceX96, protocolFee, swapFee); + + // Call after initialization hook with the resulting data + key.hooks.afterInitialize(key, sqrtPriceX96, tick, hookData); + + // Emit an event to signal the initialization of the pool with key details + emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks); + } + + // ... [other functions] +} +``` +Upon success, the transaction announces a new pool was created by emitting an `Initialize` event. + +# PoolKey + +The `PoolKey` is a structure that uniquely identifies a liquidity pool by storing its details -- the two +currencies involved (sorted numerically), the swap fee, tick spacing, and hooks (extra functionalities) of the pool. + +It acts as a unique identifier, ensuring that each pool can be precisely specified and accessed within the code. + +The liquidity for a PoolKey is unique to that pool alone + +```solidity +/// @notice Returns the key for identifying a pool +struct PoolKey { + /// @notice The lower currency of the pool, sorted numerically + Currency currency0; + /// @notice The higher currency of the pool, sorted numerically + Currency currency1; + /// @notice The pool swap fee, capped at 1_000_000. The upper 4 bits determine if the hook sets any fees. + uint24 fee; + /// @notice Ticks that involve positions must be a multiple of tick spacing + int24 tickSpacing; + /// @notice The hooks of the pool + IHooks hooks; +} +``` + +Since we create and pass the `PoolKey` to the `initialize` function, and as part of the PoolKey we pass the `hooks` we +want to use for the pool. + +We can use the `hooks` to customize the pool to our liking. + +# Initialization Code +Here are the important parts of the initialization code from the `PoolManagerInitializeTest` contract. + +https://github.com/Uniswap/v4-core/blob/main/test/PoolManagerInitialize.t.sol +```solidity + +contract Deployers { + function deployFreshManager() internal { + manager = new PoolManager(500000); + } + + function deployFreshManagerAndRouters() internal { + deployFreshManager(); + + // Initialize various routers with the deployed manager. These routers likely handle + // different aspects of the pool's functionality, such as swapping, liquidity modification, etc. + swapRouter = new PoolSwapTest(manager); + modifyLiquidityRouter = new PoolModifyLiquidityTest(manager); + donateRouter = new PoolDonateTest(manager); + takeRouter = new PoolTakeTest(manager); + initializeRouter = new PoolInitializeTest(manager); // This is the router that is used to initialize the pool + + // ... [other routers] + } +} + +contract PoolManagerInitializeTest is Test, Deployers, GasSnapshot { + function setUp() public { + deployFreshManagerAndRouters(); + + (currency0, currency1) = deployMintAndApprove2Currencies(); + + uninitializedKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: 3000, + hooks: IHooks(ADDRESS_ZERO), + tickSpacing: 60 + }); + } + + function test_initialize_succeedsWithHooks(uint160 sqrtPriceX96) public { + // Assumptions tested in Pool.t.sol + sqrtPriceX96 = uint160(bound(sqrtPriceX96, TickMath.MIN_SQRT_RATIO, TickMath.MAX_SQRT_RATIO - 1)); + + address payable mockAddr = payable(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG))); + address payable hookAddr = payable(MOCK_HOOKS); + + vm.etch(hookAddr, vm.getDeployedCode("EmptyTestHooks.sol:EmptyTestHooks")); + MockContract mockContract = new MockContract(); + vm.etch(mockAddr, address(mockContract).code); + + MockContract(mockAddr).setImplementation(hookAddr); + + uninitializedKey.hooks = IHooks(mockAddr); + + // Call initialize function with the uninitialized key and the specified sqrtPriceX96 + int24 tick = initializeRouter.initialize(uninitializedKey, sqrtPriceX96, ZERO_BYTES); + (Pool.Slot0 memory slot0,,,) = manager.pools(uninitializedKey.toId()); + assertEq(slot0.sqrtPriceX96, sqrtPriceX96, "sqrtPrice"); + + } +} +``` diff --git a/docs/contracts/v4/concepts/03-managing-position.mdx b/docs/contracts/v4/concepts/03-managing-position.mdx new file mode 100644 index 0000000000..3a7c8b2cfd --- /dev/null +++ b/docs/contracts/v4/concepts/03-managing-position.mdx @@ -0,0 +1,149 @@ +--- +id: managing-positions +title: Managing Positions +sidebar_position: 2 +--- + +# Position +To add or remove the tokens we call the function `modifyPosition` which is defined in `PoolManager.sol` file + +```solidity +struct ModifyPositionParams { + // the lower and upper tick of the position + int24 tickLower; + int24 tickUpper; + // how to modify the liquidity + int256 liquidityDelta; +} + +/// @notice Modify the position for the given pool +function modifyPosition(PoolKey memory key, ModifyPositionParams memory params, bytes calldata hookData) +external +returns (BalanceDelta); +``` + +# Important Concepts +Some of the important concepts to understand when working with Uniswap V3 or V4 positions are: + +1. Tick +2. Tick Spacing +3. SquareRoot Price X96 +4. Liquidity Delta + +### Tick +`tick` is a measure used in this code to handle prices of two different assets (tokens) in a unique way. A `tick` +represents a specific price ratio between these two assets, calculated using a mathematical formula. + +![Price And Ticks](./images/02_Managing_Position/PriceAndTicks.png) + +There are minimum and maximum `tick` values defined within the code, ensuring that calculated prices are within a +reasonable and acceptable range. + +In Uniswap V3 (and V4), liquidity providers can provide liquidity at specific price ranges (ticks), allowing them to +concentrate their liquidity and potentially earn more fees. + +Each tick corresponds to a specific price, and not all prices are represented due to the discrete nature of the ticks. + +### Tick Spacing +The tick spacing is a parameter that determines the separation between these usable ticks, making only every +Nth tick available for liquidity provision, where `N` is the tick spacing. This is a form of quantization of +the price levels that liquidity can be provided at. + +### SquareRoot Price X96 +In Uniswap V3 (and V4), the square root price (`sqrtPriceX96`) is a key concept and a crucial part of the mathematical +calculations. It's utilized for various calculations, including determining the amount of tokens that should be moved +during a swap and the liquidity calculations within specific price ranges. + +Here’s a breakdown of what `sqrtPriceX96` represents: + +### 1. **Square Root Price:** +The price is represented as the square root of the actual price ratio of the tokens. This representation +simplifies the math, especially when it comes to calculating the amounts to be swapped, as well as the +liquidity calculations within tick ranges. + +### 2. **X96:** +The X96 suffix refers to the fixed-point format used. Uniswap V3 utilizes a 96-bit fixed-point number format. +In this representation, there is a convention to maintain high precision in calculations. The fixed-point +representation means that the actual floating-point number is multiplied by \(2^{96}\) and stored as an +integer. When reading the value, it has to be interpreted properly by dividing it by \(2^{96}\) to get the +actual value. + + +![SqrtPriceX96](./images/02_Managing_Position/SqrtPriceX96.png) + +### SqrtPriceX96 to Tick +Since both the `tick` and `sqrtPriceX96` are representations of the price, they can be converted from one to the other. + +The Uniswap V3/V4 core library provides two functions to convert between the two representations: + +https://github.com/Uniswap/v4-core/blob/main/src/libraries/TickMath.sol + +1. The function `getSqrtRatioAtTick` takes a `tick` value as an input, and it computes the square root of the price +ratio of the two assets at that specific `tick`. The result represents the price relationship between two +assets in a particular state or position. + +2. The `getTickAtSqrtRatio` function does the reverse—it takes a square root of a price ratio and calculates +the corresponding `tick`. This `tick` value represents a position or state where the assets have the given +price relationship. + +![sqrtPriceX96 to Tick](./images/02_Managing_Position/sqrtPriceX96_to_tick.png) + +### Liquidity Delta +The `liquidityDelta` is the difference between the current liquidity and the desired liquidity in a position. It can +be positive (when you're adding liquidity) or negative (when you're removing liquidity). + +Here is the code from Uniswap V3 contracts which calculates the liquidity (or liquidityDelta): + +https://github.com/Uniswap/v3-periphery/blob/main/contracts/base/LiquidityManagement.sol + +```solidity + (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); + + liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + sqrtRatioAX96, + sqrtRatioBX96, + params.amount0Desired, + params.amount1Desired + ); +``` + +# Example - Add Liquidity +Here is the code that adds liquidity to a position +```solidity +int24 tickLower = -600; +int24 tickUpper = 600; +uint256 liquidity = 1e18; + +PoolManager manager = new PoolManager(500000); +// Helpers for interacting with the pool +PoolModifyPositionTest modifyPositionRouter = new PoolModifyPositionTest(IPoolManager(address(manager))); + +modifyPositionRouter.modifyPosition( + poolKey, + IPoolManager.ModifyPositionParams({ + tickLower: tickLower, + tickUpper: tickUpper, + liquidityDelta: int256(liquidity) + }), + ZERO_BYTES +); +``` +Note: `PoolModifyPositionTest` implements the `ILockCallback` interface and adds the `lockAcquired` function, which in turn calls the `manager.modifyPosition` function. + +# Acquiring Lock +Full detail about the locking mechanism is explained in the [Locking Mechanism](/03_Locking_Mechanism/README.md) section. + +The contract that calls the `modifyPosition` must implement ILockCallback interface. + +PoolModifyPositionTest.sol has some examples of how to acquire lock and some basic checks in place. +https://github.com/Uniswap/v4-core/blob/main/src/test/PoolModifyPositionTest.sol + +In `PoolModifyPositionTest` the `lockAcquired` function is executed when the lock is acquired, handling +balance adjustments and interactions with external currencies and contracts. The function takes raw +encoded data as input, which is then decoded into structured data, specifically `CallbackData`. Essential +validations and checks are performed, ensuring the caller of the function is the manager, and it +processes the modifications such as settling amounts and making necessary transfers based on +conditions like whether the amount being positive or negative. diff --git a/docs/contracts/v4/concepts/04-swap-tokens.mdx b/docs/contracts/v4/concepts/04-swap-tokens.mdx new file mode 100644 index 0000000000..ecc594d8cf --- /dev/null +++ b/docs/contracts/v4/concepts/04-swap-tokens.mdx @@ -0,0 +1,78 @@ +--- +id: swap-tokens +title: Swap Tokens +sidebar_position: 3 +--- + +# Swap - Step by Step + +The swap happens in a loop until the specified amount has been completely used or the price limit +has been reached. In each iteration, the code calculates how much of the tokens can be swapped at +the current price level. + +The swap keeps iterating until either the specified amount is fully used or the square root of +the price hits the defined limit (`sqrtPriceLimitX96`). + +Here is the `SwapParams` struct, `Swap` event and `swap` function from IPoolManager which is used to swap tokens: + +```solidity +/// @notice Emitted for swaps between currency0 and currency1 +/// @param id The abi encoded hash of the pool key struct for the pool that was modified +/// @param sender The address that initiated the swap call, and that received the callback +/// @param amount0 The delta of the currency0 balance of the pool +/// @param amount1 The delta of the currency1 balance of the pool +/// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96 +/// @param liquidity The liquidity of the pool after the swap +/// @param tick The log base 1.0001 of the price of the pool after the swap +event Swap( + PoolId indexed id, + address indexed sender, + int128 amount0, + int128 amount1, + uint160 sqrtPriceX96, + uint128 liquidity, + int24 tick, + uint24 fee +); + +struct SwapParams { + bool zeroForOne; + int256 amountSpecified; + uint160 sqrtPriceLimitX96; +} + +/// @notice Swap against the given pool +function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) + external + returns (BalanceDelta); +``` + +# Example +Here is an example of how to swap tokens by calling the `swap` function: +```solidity +PoolSwapTest swapRouter = new PoolSwapTest(IPoolManager(address(manager))); + +PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true}); + +swapRouter.swap(key, params, testSettings, hookData); +``` +Note: `PoolSwapTest` implements the `ILockCallback` interface and adds the `lockAcquired` function, which in turn calls the `manager.swap` function. + +# Acquiring Lock +Full detail about the locking mechanism is explained in the [Locking Mechanism](/03_Locking_Mechanism/README.md) section. + +The contract that calls the `swap` must implement ILockCallback interface. + +`PoolSwapTest.sol` has some examples of how to acquire lock and some basic checks in place. +https://github.com/Uniswap/v4-core/blob/main/src/test/PoolSwapTest.sol + +In `PoolSwapTest`, the `lockAcquired` function is triggered when a certain "lock" is acquired in the +context of swapping assets or tokens. Once this lock is confirmed, the function handles +the settlement based on the result of the swap. It decodes the data provided to understand the context, asks +the manager to perform the swap, and then either +transfers, withdraws, or mints tokens based on the balance changes resulting from the swap. + +To ensure secure operations, the function checks that it's only being called by the intended `manager` contract. +Depending on the type of swap and settings, it handles the balance adjustments for two types of currencies: +`currency0` and `currency1`. After settling all balances, it returns the balance changes to the caller. diff --git a/docs/contracts/v4/concepts/05-lock-mechanism.mdx b/docs/contracts/v4/concepts/05-lock-mechanism.mdx new file mode 100644 index 0000000000..ff779db957 --- /dev/null +++ b/docs/contracts/v4/concepts/05-lock-mechanism.mdx @@ -0,0 +1,192 @@ +--- +id: lock-mechanism +title: Lock Mechanism - Flash Accounting +sidebar_position: 4 +--- +# Intro to Locking +The locking mechanism in V4 ensures that certain operations are executed atomically without interference, ensuring +consistency and correctness in the PoolManager's state. PoolManager, uses `Lockers` to manage a queue of +lockers, ensuring that all currency deltas are settled before releasing a lock. + +Pool actions can be taken by acquiring a lock on the contract and implementing the `lockAcquired` callback to +then proceed with any of the following actions on the pools: + +- `swap` +- `modifyPosition` +- `donate` +- `take` +- `settle` +- `mint` + +# Main Components +In `PoolManager`, "locking" is essentially a way to ensure certain operations are coordinated and don't interfere +with each other. Here are the main components of the locking mechanism: + +### 1. **Locking and Unlocking:** +- The `lock` function is where the locking mechanism is initiated. It pushes an address tuple (address locker, address lockCaller) +to the locker array. + + ```solidity + function lock(address lockTarget, bytes calldata data) external payable override returns (bytes memory result) { + Lockers.push(lockTarget, msg.sender); + + // the caller does everything in this callback, including paying what they owe via calls to settle + result = ILockCallback(lockTarget).lockAcquired(msg.sender, data); + + if (Lockers.length() == 1) { + if (Lockers.nonzeroDeltaCount() != 0) revert CurrencyNotSettled(); + Lockers.clear(); + } else { + Lockers.pop(); + } + } + ``` + +- During the lock, a callback function `ILockCallback(lockTarget).lockAcquired(msg.sender, data)` is called, where the locked +contract can perform necessary operations. + +- After the operations in the callback are completed, it either clears the `Lockers` if it has only element, +signifying the release of the lock, or it pops the last element from `Lockers`, signifying that the lock is +released by that particular address. + +### 2. **Lockers.sol** + +The `Lockers` library stores an array/queue of locker addresses and their corresponding lock callers in transient storage. +Each locker is represented as a tuple (address locker, address lockCaller), and each tuple occupies two slots in +transient storage. The functions `push`, `pop`, `length`, and `clear` are used to manage this queue. + +### 3. **Non-Zero Deltas Tracking(Flash Accounting):** + +- The `Lockers` library also includes a mechanism to keep track of the number of nonzero deltas +through `nonzeroDeltaCount`, `incrementNonzeroDeltaCount`, and `decrementNonzeroDeltaCount`. These functions tracks +the number of changes or deltas that have occurred and these deltas are checked before releasing the lock. + + +- The `_accountDelta` function tracks the balance changes (delta) for each locker with respect to a specific currency. +It first checks if the delta (change in balance) is zero; if it is, the function +returns immediately as there's no change to account for. If there's a non-zero delta, the function retrieves the +current balance for the specified locker and currency, and then calculates the new balance by adding the delta +to the current balance. + + The function also uses the `Lockers` library to increment or decrement the `nonzeroDeltaCount` that tracks the number +of non-zero balance changes. + + ```solidity + function _accountDelta(Currency currency, int128 delta) internal { + if (delta == 0) return; + + address locker = Lockers.getCurrentLocker(); + int256 current = currencyDelta[locker][currency]; + int256 next = current + delta; + + unchecked { + if (next == 0) { + Lockers.decrementNonzeroDeltaCount(); + } else if (current == 0) { + Lockers.incrementNonzeroDeltaCount(); + } + } + + currencyDelta[locker][currency] = next; + } + ``` +- This mechanism is a key component of what is termed "Flash Accounting" in Uniswap V4. Flash Accounting is an +innovative approach introduced with the new singleton-style pool management. This feature fundamentally +alters the management of tokens during transaction processes. Traditional methods typically require explicit +tracking of token balances at every operational phase. In contrast, Flash Accounting operates under the principle +that by the end of each transaction or "lock period," there should be no net tokens owed either to the pool or the +caller, streamlining the accounting process significantly. + +### Working +The locking mechanism in the PoolManager contract works as follows: + +1. When a user wants to lock, they call the `lock()` function with the data that they want to be passed to the callback. +2. The `lock()` function pushes the tuple (address locker, address lockCaller) onto the locker queue. +3. The `lock()` function then calls the `ILockCallback(lockTarget).lockAcquired(msg.sender, data)` callback. +4. The callback can do whatever it needs to do, such as updating the user's balances or interacting with other contracts. +5. Once the callback is finished, it returns to the `lock()` function. +6. The `lock()` function checks if there are any other lockers in the queue. If there are, it pops the next locker off the queue and calls the callback for that locker. +7. If there are no more lockers in the queue, the `lock()` function returns. + +# Example +Below is the example from a community ["Liquidity Bootstrapping Hook"](https://github.com/kadenzipfel/uni-lbp/blob/main/src/LiquidityBootstrappingHooks.sol) hook that uses the locking mechanism and calls the `modifyPosition` and `swap` functions. + +```solidity +/// @notice Callback function called by the poolManager when a lock is acquired +/// Used for modifying positions and swapping tokens internally +/// @param data Data passed to the lock function +/// @return Balance delta +function lockAcquired(bytes calldata data) external override poolManagerOnly returns (bytes memory) { + bytes4 selector = abi.decode(data[:32], (bytes4)); + + if (selector == IPoolManager.modifyPosition.selector) { + ModifyPositionCallback memory callback = abi.decode(data[32:], (ModifyPositionCallback)); + + BalanceDelta delta = poolManager.modifyPosition(callback.key, callback.params, bytes("")); + + if (callback.params.liquidityDelta < 0) { + // Removing liquidity, take tokens from the poolManager + _takeDeltas(callback.key, delta, callback.takeToOwner); // Take to owner if specified (exit) + } else { + // Adding liquidity, settle tokens to the poolManager + _settleDeltas(callback.key, delta); + } + + return abi.encode(delta); + } + + if (selector == IPoolManager.swap.selector) { + SwapCallback memory callback = abi.decode(data[32:], (SwapCallback)); + + BalanceDelta delta = poolManager.swap(callback.key, callback.params, bytes("")); + + // Take and settle deltas + _takeDeltas(callback.key, delta, true); // Take tokens to the owner + _settleDeltas(callback.key, delta); + + return abi.encode(delta); + } + + return bytes(""); +} +``` + +Other important thing to note is that before the lock is released, the `nonzeroDeltaCount` is checked to ensure that +all currency deltas are settled. This is done by `_takeDeltas` and `_settleDeltas` functions. + +```solidity +/// @notice Helper function to take tokens according to balance deltas +/// @param delta Balance delta +/// @param takeToOwner Whether to take the tokens to the owner +function _takeDeltas(PoolKey memory key, BalanceDelta delta, bool takeToOwner) internal { + PoolId poolId = key.toId(); + int256 delta0 = delta.amount0(); + int256 delta1 = delta.amount1(); + + if (delta0 < 0) { + poolManager.take(key.currency0, takeToOwner ? owner[poolId] : address(this), uint256(-delta0)); + } + + if (delta1 < 0) { + poolManager.take(key.currency1, takeToOwner ? owner[poolId] : address(this), uint256(-delta1)); + } +} + +/// @notice Helper function to settle tokens according to balance deltas +/// @param key Pool key +/// @param delta Balance delta +function _settleDeltas(PoolKey memory key, BalanceDelta delta) internal { + int256 delta0 = delta.amount0(); + int256 delta1 = delta.amount1(); + + if (delta0 > 0) { + key.currency0.transfer(address(poolManager), uint256(delta0)); + poolManager.settle(key.currency0); + } + + if (delta1 > 0) { + key.currency1.transfer(address(poolManager), uint256(delta1)); + poolManager.settle(key.currency1); + } +} +``` diff --git a/docs/contracts/v4/concepts/07-deployment.mdx b/docs/contracts/v4/concepts/07-deployment.mdx new file mode 100644 index 0000000000..efa725cd8a --- /dev/null +++ b/docs/contracts/v4/concepts/07-deployment.mdx @@ -0,0 +1,196 @@ +--- +id: hook-deployment +title: Hook Deployment +sidebar_position: 6 +--- + +# Hook Deployment + +Each hook is associated with a specific flag, represented as a constant within the contract. These constants are +bit positions in an address. For instance, the `BEFORE_INITIALIZE_FLAG` is represented by a bit shift of `1 << 159`, +indicating it corresponds to the 160th bit in the address. When a hooks contract is deployed, its address's leading +bits are inspected to determine which hooks are enabled. + +The PoolManager, during initialization, calls the Hooks library to verify if the hooks are deployed at the correct addresses. + +For example, a hooks contract deployed at the address `0x9000000000000000000000000000000000000000` has leading bits +'1001'. This configuration activates the hooks corresponding to these bits, such as the 'before initialize' and 'after +add liquidity' hooks. This approach provides a compact and efficient method for encoding permissions directly into +the contract's address. + +This encoding indicates that two specific hooks ('before initialize' and 'after add liquidity') will be triggered. In the provided address: +- The leading `1` at the second-highest position aligns with the `BEFORE_INITIALIZE_FLAG` (bit 159), and +- The trailing `1` in the sequence `1001` aligns with the `AFTER_ADD_LIQUIDITY_FLAG` (bit 156). + +The other two `0`s in the sequence indicate that the `AFTER_INITIALIZE_FLAG` and `BEFORE_ADD_LIQUIDITY_FLAG` are not set. + + +| Hex Hook Address | Binary Address | Description | +|---------------------------------------------|---------------------|---------------------------| +| `0x8000000000000000000000000000000000000000` | `1000 0000...` (bit 159) | BEFORE_INITIALIZE_FLAG | +| `0x4000000000000000000000000000000000000000` | `0100 0000...` (bit 158) | AFTER_INITIALIZE_FLAG | +| `0x2000000000000000000000000000000000000000` | `0010 0000...` (bit 157) | BEFORE_ADD_LIQUIDITY_FLAG | +| `0x1000000000000000000000000000000000000000` | `0001 0000...` (bit 156) | AFTER_ADD_LIQUIDITY_FLAG | +| `0x0800000000000000000000000000000000000000` | `0000 1000...` (bit 155) | BEFORE_REMOVE_LIQUIDITY_FLAG| +| `0x0400000000000000000000000000000000000000` | `0000 0100...` (bit 154) | AFTER_REMOVE_LIQUIDITY_FLAG | +| `0x0200000000000000000000000000000000000000` | `0000 0010...` (bit 153) | BEFORE_SWAP_FLAG | +| `0x0100000000000000000000000000000000000000` | `0000 0001...` (bit 152) | AFTER_SWAP_FLAG | +| `0x0080000000000000000000000000000000000000` | `0000 0000 1000...` (bit 151) | BEFORE_DONATE_FLAG | +| `0x0040000000000000000000000000000000000000` | `0000 0000 0100...` (bit 150) | AFTER_DONATE_FLAG | +| `0x0020000000000000000000000000000000000000` | `0000 0000 0010...` (bit 149) | NO_OP_FLAG | +| `0x0010000000000000000000000000000000000000` | `0000 0000 0001...` (bit 148) | ACCESS_LOCK_FLAG | + + +To generate valid hook addresses based on the code provided, we focus on the leading bits that indicate +which hooks are invoked. Each flag corresponds to specific leading bits in the address, as indicated by +the constants provided. + + +Here are some example addresses based on the flags: + + +### 1. One Hook +#### **Example 1: Just BEFORE_SWAP_FLAG** +- Address: `0x2000000000000000000000000000000000000000` +- Leading bits: '0010...' +- Explanation: The flag for "before swap" is set by having a '1' in the 153rd bit (from the right), represented by `0x2000000000000000000000000000000000000000` in hexadecimal. + +#### **Example 2: Just AFTER_DONATE_FLAG** +- Address: `0x4000000000000000000000000000000000000000` +- Leading bits: '0100...' +- Explanation: The flag for "after donate" is set by having a '1' in the 150th bit, indicated by `0x4000000000000000000000000000000000000000`. + +### 2. Two Hooks +#### **Example 3: BEFORE_SWAP_FLAG and AFTER_SWAP_FLAG** +- Address: `0x3000000000000000000000000000000000000000` +- Leading bits: '0011...' +- Explanation: Combining flags for "before swap" and "after swap" requires setting bits 153 and 152, resulting in `0x3000000000000000000000000000000000000000`. + +#### **Example 4: BEFORE_INITIALIZE_FLAG and AFTER_INITIALIZE_FLAG** +- Address: `0xC000000000000000000000000000000000000000` +- Leading bits: '1100...' +- Explanation: The combination of "before initialize" and "after initialize" sets bits 159 and 158, represented by `0xC000000000000000000000000000000000000000`. + + +```solidity +library Hooks { + // These flags are defined using bitwise left shifts. The `1 << n` operation means that the binary number `1` is shifted + // to the left by `n` positions, effectively placing a `1` at the `n`th bit position (counting from the right and + // starting from 0). This technique is commonly used in programming to set a specific bit in a number, which can be used + // as a flag. In Ethereum addresses, which are 160 bits long, these flags correspond to the leading bits because of the + // high positions of the shift (e.g., 159, 158). + + uint256 internal constant BEFORE_INITIALIZE_FLAG = 1 << 159; // (Bit 159) + uint256 internal constant AFTER_INITIALIZE_FLAG = 1 << 158; // (Bit 158) + uint256 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 157; // (Bit 157) + uint256 internal constant AFTER_ADD_LIQUIDITY_FLAG = 1 << 156; // (Bit 156) + uint256 internal constant BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 155; // (Bit 155) + uint256 internal constant AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 154; // (Bit 154) + uint256 internal constant BEFORE_SWAP_FLAG = 1 << 153; // (Bit 153) + uint256 internal constant AFTER_SWAP_FLAG = 1 << 152; // (Bit 152) + uint256 internal constant BEFORE_DONATE_FLAG = 1 << 151; // (Bit 151) + uint256 internal constant AFTER_DONATE_FLAG = 1 << 150; // (Bit 150) + uint256 internal constant NO_OP_FLAG = 1 << 149; // (Bit 149) + uint256 internal constant ACCESS_LOCK_FLAG = 1 << 148; // (Bit 148) + + + /// @notice Utility function intended to be used in hook constructors to ensure + /// the deployed hooks address causes the intended hooks to be called + /// @param permissions The hooks that are intended to be called + /// @dev permissions param is memory as the function will be called from constructors + function validateHookPermissions(IHooks self, Permissions memory permissions) internal pure { + if ( + permissions.beforeInitialize != self.hasPermission(BEFORE_INITIALIZE_FLAG) + || permissions.afterInitialize != self.hasPermission(AFTER_INITIALIZE_FLAG) + || permissions.beforeAddLiquidity != self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG) + || permissions.afterAddLiquidity != self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG) + || permissions.beforeRemoveLiquidity != self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG) + || permissions.afterRemoveLiquidity != self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG) + || permissions.beforeSwap != self.hasPermission(BEFORE_SWAP_FLAG) + || permissions.afterSwap != self.hasPermission(AFTER_SWAP_FLAG) + || permissions.beforeDonate != self.hasPermission(BEFORE_DONATE_FLAG) + || permissions.afterDonate != self.hasPermission(AFTER_DONATE_FLAG) + || permissions.noOp != self.hasPermission(NO_OP_FLAG) + || permissions.accessLock != self.hasPermission(ACCESS_LOCK_FLAG) + ) { + revert HookAddressNotValid(address(self)); + } + } + + function hasPermission(IHooks self, uint256 flag) internal pure returns (bool) { + return uint256(uint160(address(self))) & flag != 0; + } +} +``` + + + +# CREATE2 +Ethereum blockchain allows you to create contracts. There are two ways to create +these contracts: + +1. **CREATE**: This is the regular way. Every time you create a contract using this, it gets a new, unique +address (like a house getting a unique postal address). + +2. **CREATE2**: This is a advanced way. Here, you use your address, a `salt` which is a unique number you choose, and +the contract's code called `bytecode` to create the contract. The magic of `CREATE2` is that if you use the same +fields, you'll get the same contract address every time. + +Using `CREATE2` helps ensure that the hook is deployed to the exact right address. + +Here's a small code that predicts the address where a contract will be deployed using `CREATE2` before actually +deploying it. +```solidity +bytes32 salt = keccak256(abi.encodePacked(someData)); +address predictedAddress = address(uint(keccak256(abi.encodePacked( + byte(0xff), + deployerAddress, + salt, + keccak256(bytecode) +)))); +``` + + +# Deterministic Deployment Proxy +Many developers use https://github.com/Arachnid/deterministic-deployment-proxy to deploy contracts to a specific +address. The main feature of this project is the use of the Ethereum CREATE2 opcode, which allows for deterministic +deployment of contracts. The deployment proxy also enables the same address across different networks. + +Most of the chains do have the deployment proxy at `0x4e59b44847b379578588920cA78FbF26c0B4956C`. See [here](https://github.com/Uniswap/v4-periphery/issues/59#issuecomment-1716379675) +for more details. + +# Hook Deployment Code +The https://github.com/uniswapfoundation/v4-template repository contains some helper utilities for deploying hooks. + +Here is the code for deploying the hooks using Deterministic Deployment Proxy which is deployed at `0x4e59b44847b379578588920cA78FbF26c0B4956C`: +```solidity +contract CounterScript is Script { + address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + address constant GOERLI_POOLMANAGER = address(0x3A9D48AB9751398BbFa63ad67599Bb04e4BdF98b); + + function setUp() public {} + + function run() public { + // hook contracts must have specific flags encoded in the address + uint160 flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + ); + + // Mine a salt that will produce a hook address with the correct flags + (address hookAddress, bytes32 salt) = + HookMiner.find(CREATE2_DEPLOYER, flags, type(Counter).creationCode, abi.encode(address(GOERLI_POOLMANAGER))); + + // Deploy the hook using CREATE2 + vm.broadcast(); + Counter counter = new Counter{salt: salt}(IPoolManager(address(GOERLI_POOLMANAGER))); + require(address(counter) == hookAddress, "CounterScript: hook address mismatch"); + } +} +``` +https://github.com/uniswapfoundation/v4-template/blob/main/script/CounterDeploy.s.sol + +Note: This is a Foundry script, and it won't work for hardhat. + + +Read more about deploying your own hooks [here](/contracts/v4/first-hook/hook-deployment). diff --git a/docs/contracts/v4/concepts/_category_.json b/docs/contracts/v4/concepts/_category_.json new file mode 100644 index 0000000000..74b831d116 --- /dev/null +++ b/docs/contracts/v4/concepts/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Concepts", + "position": 2, + "collapsed": false +} diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png new file mode 100644 index 0000000000..84d466c9a0 Binary files /dev/null and b/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png differ diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_detailed_architecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_detailed_architecture.png new file mode 100644 index 0000000000..3001b623fc Binary files /dev/null and b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_detailed_architecture.png differ diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_high_level_architecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_high_level_architecture.png new file mode 100644 index 0000000000..2bd471a1bd Binary files /dev/null and b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_high_level_architecture.png differ diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_detailed_architecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_detailed_architecture.png new file mode 100644 index 0000000000..6e93648525 Binary files /dev/null and b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_detailed_architecture.png differ diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_high_level_architecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_high_level_architecture.png new file mode 100644 index 0000000000..acbeb32b05 Binary files /dev/null and b/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_high_level_architecture.png differ diff --git a/docs/contracts/v4/concepts/images/02_Managing_Position/PriceAndTicks.png b/docs/contracts/v4/concepts/images/02_Managing_Position/PriceAndTicks.png new file mode 100644 index 0000000000..65755db541 Binary files /dev/null and b/docs/contracts/v4/concepts/images/02_Managing_Position/PriceAndTicks.png differ diff --git a/docs/contracts/v4/concepts/images/02_Managing_Position/SqrtPriceX96.png b/docs/contracts/v4/concepts/images/02_Managing_Position/SqrtPriceX96.png new file mode 100644 index 0000000000..87fd7764ee Binary files /dev/null and b/docs/contracts/v4/concepts/images/02_Managing_Position/SqrtPriceX96.png differ diff --git a/docs/contracts/v4/concepts/images/02_Managing_Position/sqrtPriceX96_to_tick.png b/docs/contracts/v4/concepts/images/02_Managing_Position/sqrtPriceX96_to_tick.png new file mode 100644 index 0000000000..f4fb57d763 Binary files /dev/null and b/docs/contracts/v4/concepts/images/02_Managing_Position/sqrtPriceX96_to_tick.png differ diff --git a/docs/contracts/v4/concepts/images/03_fee_calculation/FeeCalculation.png b/docs/contracts/v4/concepts/images/03_fee_calculation/FeeCalculation.png new file mode 100644 index 0000000000..b3465bd19f Binary files /dev/null and b/docs/contracts/v4/concepts/images/03_fee_calculation/FeeCalculation.png differ diff --git a/docs/contracts/v4/concepts/images/05_locking_mechanism/LockingMechanism_excali.png b/docs/contracts/v4/concepts/images/05_locking_mechanism/LockingMechanism_excali.png new file mode 100644 index 0000000000..3f09b44a7b Binary files /dev/null and b/docs/contracts/v4/concepts/images/05_locking_mechanism/LockingMechanism_excali.png differ diff --git a/docs/contracts/v4/concepts/images/05_locking_mechanism/locking_mechanism.png b/docs/contracts/v4/concepts/images/05_locking_mechanism/locking_mechanism.png new file mode 100644 index 0000000000..e502cd1720 Binary files /dev/null and b/docs/contracts/v4/concepts/images/05_locking_mechanism/locking_mechanism.png differ diff --git a/docs/contracts/v4/concepts/images/Liquidity1.png b/docs/contracts/v4/concepts/images/Liquidity1.png new file mode 100644 index 0000000000..719d0af81a Binary files /dev/null and b/docs/contracts/v4/concepts/images/Liquidity1.png differ diff --git a/docs/contracts/v4/first-hook/01-building-you-hook.mdx b/docs/contracts/v4/first-hook/01-building-you-hook.mdx new file mode 100644 index 0000000000..dbb282c588 --- /dev/null +++ b/docs/contracts/v4/first-hook/01-building-you-hook.mdx @@ -0,0 +1,125 @@ +--- +id: building-your-own-hook +title: Building your own hook +sidebar_position: 1 +--- + +# Build your own hook +Whenever starting on a new project, it is always a good idea to use a framework which can assist in development and +that can make the setup process easier. Similarly, when building your own hooks, will would want to use a few utilities to +1) Mining the correct salt for your hook address +2) Using a router which acquire the lock and then call various functions on the pool manager +3) Deploying the hook to a deterministic address + +Many of these steps are already taken care by a few template repositories which are listed [here](https://uniswaphooks.com/components/hooks/templates) + +## Hooks Template +One of the most popular templates is the [v4-template](https://github.com/uniswapfoundation/v4-template) which is +created by [Sauce](https://github.com/saucepoint). + +You can clone the repository and then follow the instructions in the README to get started. + +## Getting Started +1. **Installation**: The template requires Foundry. Install dependencies using: +```shell +forge install +``` +2. **Testing**: Run tests with: +```shell +forge test +``` +## Counter Hook +The template includes an example hook (`Counter.sol`), a test for it, and scripts for deploying hooks. + +Here are some of the things to note about the Counter hook: + +1. It extends the `BaseHook` contract which is defined in the [v4-periphery](https://github.com/Uniswap/v4-periphery/blob/main/contracts/BaseHook.sol) repository. +```solidity +import {BaseHook} from "v4-periphery/BaseHook.sol"; + +contract Counter is BaseHook { + // ... hook code here +} + +``` +2. The hook gets called before and after every swap and modify position call. This is done as part of `getHookPermissions` function + +```solidity +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, + noOp: false, + accessLock: false + }); +} + +``` + +3. Corresponding to the hooks calls, the hook implements the following functions: +```solidity + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4) + { + beforeSwapCount[key.toId()]++; + return BaseHook.beforeSwap.selector; + } + + function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4) + { + afterSwapCount[key.toId()]++; + return BaseHook.afterSwap.selector; + } + + 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; + } +``` +4. In each of these functions, Counter hook increments a counter. You can see this in the `beforeSwap` function: +```solidity +function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4) +{ + beforeSwapCount[key.toId()]++; + return BaseHook.beforeSwap.selector; +} +``` + +## Additional Resources +- [v4-periphery](https://github.com/Uniswap/v4-periphery): For advanced hook implementations. +- [v4-core](https://github.com/Uniswap/v4-core): The core repository for Uniswap v4. + + + + diff --git a/docs/contracts/v4/first-hook/02-hook-testing.mdx b/docs/contracts/v4/first-hook/02-hook-testing.mdx new file mode 100644 index 0000000000..a63e5acfac --- /dev/null +++ b/docs/contracts/v4/first-hook/02-hook-testing.mdx @@ -0,0 +1,102 @@ +--- +id: testing-hooks +title: Testing Hooks +sidebar_position: 2 +--- + +## Testing hooks +Testing hooks is same as testing contracts. The template includes a test for the Counter hook, which you can find in `test/Counter.t.sol`. + +Here are some key points about the Counter hook test: +1. The hook extends from a couple of utilities that facilitate easier testing of hooks. + + ```solidity + import "forge-std/Test.sol"; + import {Deployers} from "v4-core/test/utils/Deployers.sol"; + + contract CounterTest is Test, Deployers { + } + ``` + +2. The `setup` function, called before every test, creates a few test tokens, retrieves the hook address, and then initializes the pool with this hook address. + + ```solidity + function setup() public { + Deployers.deployFreshManagerAndRouters(); + (currency0, currency1) = Deployers.deployMintAndApprove2Currencies(); + + // Deploy the hook to an address with the correct flags + uint160 flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + ); + (address hookAddress, bytes32 salt) = + HookMiner.find(address(this), flags, type(Counter).creationCode, abi.encode(address(manager))); + counter = new Counter{salt: salt}(IPoolManager(address(manager))); + } + ``` + + Pool is then initialized containing this hook + ```solidity + // Create the pool + key = PoolKey(currency0, currency1, 3000, 60, IHooks(counter)); + poolId = key.toId(); + initializeRouter.initialize(key, SQRT_RATIO_1_1, ZERO_BYTES); + ``` + +3. Hook tests utilize a router, namely `PoolModifyPositionTest`, to modify positions. PoolModifyPositionTest implements the `ILockCallback` interface and adds the `lockAcquired` function, which in turn calls the `manager.modifyPosition` function. + ```solidity + PoolManager manager; + PoolModifyPositionTest modifyPositionRouter; + + manager = new PoolManager(500000); + + // Helpers for interacting with the pool + modifyPositionRouter = new PoolModifyPositionTest(IPoolManager(address(manager))); + + modifyPositionRouter.modifyPosition(poolKey, IPoolManager.ModifyPositionParams(-120, 120, 10 ether), ZERO_BYTES); + ``` + Similarly, for token swaps, the test uses `PoolSwapTest`, which also implements the `ILockCallback` interface. + +4. Testing the hook closely resembles testing any other smart contract. The function `testCounterHooks` executes swaps and verifies if the counters are updated correctly. + + ```solidity + function testCounterHooks() public { + // positions were created in setup() + assertEq(counter.beforeAddLiquidityCount(poolId), 3); + assertEq(counter.beforeRemoveLiquidityCount(poolId), 0); + + assertEq(counter.beforeSwapCount(poolId), 0); + assertEq(counter.afterSwapCount(poolId), 0); + + // Perform a test swap // + bool zeroForOne = true; + int256 amountSpecified = 1e18; + BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); + // ------------------- // + + assertEq(int256(swapDelta.amount0()), amountSpecified); + + assertEq(counter.beforeSwapCount(poolId), 1); + assertEq(counter.afterSwapCount(poolId), 1); + } + + /// Test Helper + function swap( + PoolKey memory key, + bool zeroForOne, + int256 amountSpecified, + bytes memory hookData + ) internal returns (BalanceDelta delta) { + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1 // unlimited impact + }); + + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({withdrawTokens: true, settleUsingTransfer: true, currencyAlreadySent: false}); + + delta = swapRouter.swap(key, params, testSettings, hookData); + } + ``` diff --git a/docs/contracts/v4/first-hook/03-hook-deployment.mdx b/docs/contracts/v4/first-hook/03-hook-deployment.mdx new file mode 100644 index 0000000000..1aa83aa6ce --- /dev/null +++ b/docs/contracts/v4/first-hook/03-hook-deployment.mdx @@ -0,0 +1,122 @@ +--- +id: hook-deployment +title: Hook Deployment +sidebar_position: 3 +--- + +# Deployment +Deploying Uniswap V4 Hooks involves several steps: + +1. **Deploying the PoolManager Contract**: This contract is typically pre-deployed on many test environments. However, + you have the option to deploy it locally on your machine if required. + +2. **Deploying the Hook Contract**: The hook contract needs to be deployed at a predetermined address. You can use + `CREATE2` for deterministic deployment. A Deterministic Deployment Proxy, usually found + at `0x4e59b44847b379578588920cA78FbF26c0B4956C`, is employed for this purpose and is already available in most + environments. + +3. **Deploying Test Tokens**: These tokens are essential for creating the pool. They need to be deployed before + initializing the pool. + +4. **Initializing the Pool with the Hook Contract Address**: This is achieved by invoking + the `initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)` function on the PoolManager contract. + +5. **Adding Liquidity or Modifying Position**: If you wish to add liquidity to the pool or alter its position, a + utility contract that implements the `ILockCallback` interface is necessary. You may consider deploying a utility + contract like `PoolModifyPositionTest` for these operations. + + +## Deployment Scripts +The template includes a few scripts that help with deploying hooks. These scripts are located in the `scripts` folder. + +Lets look at these scripts one by one: + +### 1. Deploying Your Own Tokens +The template includes Mock UNI and Mock USDC contracts for testing. Deploy them using: +```shell + forge create script/mocks/mUNI.sol:MockUNI \ + --rpc-url [your_rpc_url_here] \ + --private-key [your_private_key_on_goerli_here] \ + + forge create script/mocks/mUSDC.sol:MockUSDC \ + --rpc-url [your_rpc_url_here] \ + --private-key [your_private_key_on_goerli_here] \ +``` + + +### 2. script/01_CreatePool.s.sol +This script contains the steps for initializing the pool with an existing hook. It uses the pre-deployed PoolManager contract and +token contracts +```solidity +contract CreatePoolScript is Script { + using CurrencyLibrary for Currency; + + //addresses with contracts deployed + address constant GOERLI_POOLMANAGER = address(0x3A9D48AB9751398BbFa63ad67599Bb04e4BdF98b); //pool manager deployed to GOERLI + address constant MUNI_ADDRESS = address(0xbD97BF168FA913607b996fab823F88610DCF7737); //mUNI deployed to GOERLI -- insert your own contract address here + address constant MUSDC_ADDRESS = address(0xa468864e673a807572598AB6208E49323484c6bF); //mUSDC deployed to GOERLI -- insert your own contract address here + address constant HOOK_ADDRESS = address(0x3CA2cD9f71104a6e1b67822454c725FcaeE35fF6); //address of the hook contract deployed to goerli -- you can use this hook address or deploy your own! + + IPoolManager manager = IPoolManager(GOERLI_POOLMANAGER); + + function run() external { + // sort the tokens! + address token0 = uint160(MUSDC_ADDRESS) < uint160(MUNI_ADDRESS) ? MUSDC_ADDRESS : MUNI_ADDRESS; + address token1 = uint160(MUSDC_ADDRESS) < uint160(MUNI_ADDRESS) ? MUNI_ADDRESS : MUSDC_ADDRESS; + uint24 swapFee = 4000; + int24 tickSpacing = 10; + + // floor(sqrt(1) * 2^96) + uint160 startingPrice = 79228162514264337593543950336; + + bytes memory hookData = abi.encode(block.timestamp); + + PoolKey memory pool = PoolKey({ + currency0: Currency.wrap(token0), + currency1: Currency.wrap(token1), + fee: swapFee, + tickSpacing: tickSpacing, + hooks: IHooks(HOOK_ADDRESS) + }); + + // Turn the Pool into an ID so you can use it for modifying positions, swapping, etc. + PoolId id = PoolIdLibrary.toId(pool); + bytes32 idBytes = PoolId.unwrap(id); + + console.log("Pool ID Below"); + console.logBytes32(bytes32(idBytes)); + + vm.broadcast(); + manager.initialize(pool, startingPrice, hookData); + } +} +``` +### 3. script/00_Counter.s.sol +This script deploys the Counter hook using Deterministic Deployment Proxy. It uses the pre-deployed PoolManager contract +and proxy + +```solidity +contract CounterScript is Script { + address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + address constant GOERLI_POOLMANAGER = address(0x3A9D48AB9751398BbFa63ad67599Bb04e4BdF98b); + + function setUp() public {} + + function run() public { + // hook contracts must have specific flags encoded in the address + uint160 flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + ); + + // Mine a salt that will produce a hook address with the correct flags + (address hookAddress, bytes32 salt) = + HookMiner.find(CREATE2_DEPLOYER, flags, type(Counter).creationCode, abi.encode(address(GOERLI_POOLMANAGER))); + + // Deploy the hook using CREATE2 + vm.broadcast(); + Counter counter = new Counter{salt: salt}(IPoolManager(address(GOERLI_POOLMANAGER))); + require(address(counter) == hookAddress, "CounterScript: hook address mismatch"); + } +} +``` diff --git a/docs/contracts/v4/first-hook/_category_.json b/docs/contracts/v4/first-hook/_category_.json new file mode 100644 index 0000000000..2b5931eddd --- /dev/null +++ b/docs/contracts/v4/first-hook/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "My First Hook", + "position": 3, + "collapsed": false +} diff --git a/docs/contracts/v4/overview.mdx b/docs/contracts/v4/overview.mdx new file mode 100644 index 0000000000..cef18ea325 --- /dev/null +++ b/docs/contracts/v4/overview.mdx @@ -0,0 +1,28 @@ +--- +id: overview +title: Overview +sidebar_position: 1 +--- + +# Uniswap V4 - Hooks + +Welcome to the Uniswap V4 developer documentation. + +Uniswap V4 is designed to enhance the way liquidity is provided and tokens are traded on-chain. It aims to broaden the +scope of decentralized trading. The project welcomes community participation, providing a platform for developers and +enthusiasts to contribute to this new version of the Uniswap Protocol. + +A significant feature in Uniswap V4 is the introduction of "hooks." These contracts operate at different stages in a +pool action's lifecycle, allowing for a high degree of customization. + +Pool creators have the option to maintain the tradeoff decisions available in V3 or to explore new features. +Uniswap V4 pools can support dynamic fees, on-chain limit orders, or act as a time-weighted average market +maker (TWAMM) to spread large orders over time. The architecture has been updated, including a new "singleton" +contract where all pools are contained within a single smart contract, enhancing the platform's efficiency and +reducing costs. + +The integration of hooks with the singleton architecture is expected to make the platform more powerful and +versatile, ensuring fast, secure, and efficient customization and routing across various pools. + +Note: V4 is currently in development, and changes are ongoing in the contracts. Updates to the documentation +will be provided accordingly.