diff --git a/02-overview.mdx b/02-overview.mdx new file mode 100644 index 0000000000..c0e0a9b5e5 --- /dev/null +++ b/02-overview.mdx @@ -0,0 +1,7 @@ +--- +title: Overview +--- + +# Overview + +(TODO: explain core contracts, and how users should expect to use them) \ No newline at end of file diff --git a/docs/contracts/v3/_category_.json b/docs/contracts/v3/_category_.json index 74d7182c61..99fed98ce4 100644 --- a/docs/contracts/v3/_category_.json +++ b/docs/contracts/v3/_category_.json @@ -1,5 +1,5 @@ { "label": "V3 Protocol", "position": 2, - "collapsed": false + "collapsed": true } diff --git a/docs/contracts/v4/concepts/01-intro-to-v4.mdx b/docs/contracts/v4/concepts/01-intro-to-v4.mdx deleted file mode 100644 index c2a3d1d03f..0000000000 --- a/docs/contracts/v4/concepts/01-intro-to-v4.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -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/01-v4-vs-v3.mdx b/docs/contracts/v4/concepts/01-v4-vs-v3.mdx new file mode 100644 index 0000000000..7171589090 --- /dev/null +++ b/docs/contracts/v4/concepts/01-v4-vs-v3.mdx @@ -0,0 +1,63 @@ +--- +title: V4 vs V3 +--- + +While Uniswap v4's underlying concentrated liquidity is the same as Uniswap v3, +there are some key differences in the architecture and accounting. + +# Singleton Design + +### Pool Creation + +**V4**: The singleton contract facilitates the creation of a pool and +also stores its state. This pattern reduces costs when creating a pool +and doing multi-hop swaps. Because pools are _contract state_ and not entirely new _contracts_ themselves, pool creation is significantly cheaper. + +**V3**: A factory contract is responsible for pool creation. The pool is +a separate contract instance that manages its own state. Pool initialization +is costly because contract creation is gas-intensive + +### Flash Accounting + +**V4**: The singleton uses _flash accounting_, meaning a caller that unlocks the PoolManager +is allowed to cause balance-changing operations (multiple swaps, multiple liquidity modifications, etc) +and only needs to perform token transfers at the very end of the sequence. + +**V3**: Because flash accounting is missing from V3, it is the responsibility +of the integrating contract to perform token transfers, after each individual call, to each individual pool contract + +# Liquidity Fee Accounting + +**V4**: Accrued fees act like a credit when modifying liquidity. +Increasing liquidity will convert the fee revenue to liquidity +inside the position while decreasing liquidity will automatically +require the withdrawal of unclaimed fee revenue. + +An additional parameter _salt_ can be provided when creating liquidity. The +_salt_ is used to distinguish positions of the same range on the same pool. +This separation may be preferred to simplify fee accounting. If two users share the same +range and state in `PoolManager`, integrating contracts must be careful in managing +fees + +**V3**: Liquidity positions of the same range and pool will share the same state. While believed to +be more gas efficient at the time, integrating contracts will need to handle fee management since +the state is shared on the core pool contract + +# Native ETH + +**V4**: Pool pairs support native tokens, in doing so ETH swappers and +liquidity providers benefit from gas cost reductions from cheaper +transfers and removal of additional wrapping costs. + +**V3**: ETH needs to be wrapped first before being paired with other tokens. +This results in higher gas costs because of wrapping and transferring +a wrapped native token. + +# Subscribers + +Only V4: Owners can now set a subscriber for their positions. +A subscriber contract will get notified every time the position's liquidity +or owner changes. Subscribers enable staking / liquidity-mining, but users do not need +to transfer their ERC-721 token. + +**V3**: Staking in v3 requires users to transfer their ERC-721 token to a contract, putting the underlying assets at risk for malicious behavior. \ No newline at end of file diff --git a/docs/contracts/v4/concepts/02-1-overview.mdx b/docs/contracts/v4/concepts/02-1-overview.mdx deleted file mode 100644 index 3957c1f9b7..0000000000 --- a/docs/contracts/v4/concepts/02-1-overview.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -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 deleted file mode 100644 index c714072d7f..0000000000 --- a/docs/contracts/v4/concepts/02-2-pool-manager-and-initialization.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -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/02-flash-accounting.mdx b/docs/contracts/v4/concepts/02-flash-accounting.mdx new file mode 100644 index 0000000000..cf7dc661b5 --- /dev/null +++ b/docs/contracts/v4/concepts/02-flash-accounting.mdx @@ -0,0 +1,73 @@ +--- +title: Flash Accounting +--- + +# Flash Accounting + +In previous versions of Uniswap, every time a swap was made - including multi-hop swap - tokens were transferred between Pool contracts for intermediate steps. + +This design incurred inefficiencies because transferring tokens with external calls to their smart contracts - especially in a multi-hop swap - is quite expensive. This design was required since each pool was its own contract and token transfers were required to maintain accounting and solvency + +With the singleton architecture, a better design was possible and is referred to as _Flash Accounting_. The design became practical with gas efficiencies of [Transient Storage](https://eips.ethereum.org/EIPS/eip-1153). _Flash Accounting_ further reduces the gas cost of trades that cross multiple pools and supports more complex integrations with Uniswap v4. + +With _flash accounting_, each balance-changing operation (e.g. swap and liquidity modification) updates an internal net balance known as `delta`. Only the final balance-changes require token transfers. + +## Deltas + +### Locking + +To ensure correctness and atomicity in complex operations like a multi-hop swap - v4 uses a locking mechanism. Anytime key actions need to take place within the `PoolManager` - e.g. swaps and liquidity modification - a periphery contract must `unlock` the `PoolManager` first. Then integrators implement the `unlockCallback` and proceed with any of the following actions on the pools: + +- swap +- modifyLiquidity +- donate +- take +- settle +- mint +- burn +- sync + +Note that pool initialization can happen outside the context of unlocking the `PoolManager`, as there are no balance-changing operations associated with pool creation. + +### Balance Delta + +Inside `unlockCallback`, a periphery contract performs balance-changing operations i.e. conduct swaps, modify positions, etc. After returning execution context back to `PoolManager`, the core contract checks that balances are resolved - nothing is owed to or from the `PoolManager`. + +The balances resolved above is what we refer as the `delta`, a field held in the _transient_ state. The value(s) represent the _debts_ and _credits_ of assets owed to or from the `PoolManager`. + + +## Swapping + +![Multi-hop swaps on V3 vs V4](https://lh7-us.googleusercontent.com/sE7quPXvlU_AKTzNWiB2aYSmlO9gbjJyHRyQyx0ljMiFTVNaxb2WZ4Zlbe1l0C25vHO-Y-3y_zQeXJYVXCYxvNWPYX4nOC6JSSOeDPzx6bqoqT74-IpBMLq7miNl3yXbgU7EE_U6tuRzpPN5JsWQWA4oKg=s2048) + +As shown in the above diagram, for example - let's say you wanted to swap `ETH` for `DAI`. Assuming this requires a multi-hop swap going from `ETH` to `USDC` and then from `USDC` to `DAI`. + +### Previously on v3 + +1. `ETH` is transferred to `ETH <> USDC` pool contract + +2. `USDC` is withdrawn from `ETH <> USDC` contract and transferred to `USDC <> DAI` contract + +3. `DAI` is withdrawn from `USDC <> DAI` contract and transferred to the user + +### Now on v4 + +1. Call `swap()` on `ETH <> USDC` + +2. Call `swap()` on `USDC <> DAI`, with the credit of USDC from above being used as the input amount + +4. User _resolves deltas_ by paying ETH and receiving DAI + +Therefore we can skip the step of actually calling `transfer()` on the USDC contract. + +The optimization scales infinitely, any number of arbitrary hops only requires two token transfers - input and output tokens. + +## Liquidity Management + +The optimization becomes more evident for complex liquidity operations + +For example, a user wanted to add liquidity to `ETH <> DAI` but does not have DAI. The user can swap some `ETH` to `DAI` in order to add liquidity with both tokens. In addition, the user can multi-hop swap going from `ETH` to `USDC` to `DAI`. If properly integrated, the user would only need to transfer ETH _once_. + + +## Developer resources +To see how unlock callback and delta work in a smart contract read [Unlock Callback & Deltas](/contracts/v4/guides/unlock-callback). \ No newline at end of file diff --git a/docs/contracts/v4/concepts/03-erc6909.mdx b/docs/contracts/v4/concepts/03-erc6909.mdx new file mode 100644 index 0000000000..3a7b14674c --- /dev/null +++ b/docs/contracts/v4/concepts/03-erc6909.mdx @@ -0,0 +1,41 @@ +--- +title: ERC-6909 +--- + +Uniswap v4 uses [ERC-6909](https://eips.ethereum.org/EIPS/eip-6909) to further improve gas-efficiency on token claims and redemptions. + +ERC-6909 is a minimal and gas-efficient standard for managing multiple ERC-20 tokens from a single contract. It provides a simplified alternative to the more complex ERC-1155 multi-token standard. + +### ERC-6909 vs ERC-1155 + +ERC-6909 offers several advantages over ERC-1155: +1. Simplified interface: ERC-6909 removes unnecessary safe transfer callbacks and batching constraints presented in ERC-1155. +2. Improved transfer delegation: ERC-6909 provides a more efficient system for transfer delegation. +3. Gas efficiency: ERC-6909 reduces gas costs for deployment, transfers, and burning operations. +4. Reduced code size: Implementing ERC-6909 results in smaller contract sizes compared to ERC-1155. + +However, it's worth noting that ERC-6909 does introduce a `totalSupply` variable, which leads to an additional disk write on mint and burn operations. + +# How it works + +Instead of choosing to move tokens in/out of the `PoolManager`, developers can opt-in and leave the ERC-20 tokens within the `PoolManager`. In exchange, the `PoolManager` can **mint them an ERC-6909 token representing their claim**. In subsequent interactions requiring _paying_ tokens, users will not need to transfer ERC-20 tokens into the `PoolManager` - users can simply _burn_ some (or all) of their claim tokens they have + + +Doing _real_ ERC-20 token transfers requires calls to external smart contracts - incurring gas overhead compared to internal accounting. Secondly, these external smart contracts have their own custom logic within their `transfer` functions - for example USDC's blocked-address list - which is a further gas overhead. Thus, minting and burning ERC-6909 tokens are more gas-efficient because they don't require external function calls and have a constant-size gas overhead regardless of the underlying ERC-20 token. + +This mechanism therefore helps further reduce gas costs. All these gas cost reductions overall make pools much more competitive based on the fees they charge. + +# Examples + +## High-frequency traders / MEV bots + +These users are often conducting a lot of swaps in relatively short durations of time, while staying within the Uniswap Protocol. These power-users can trade using ERC-6909 tokens for improved gas-efficiency. + + +## Liquidity management + +ERC-6909 does not only benefit swappers. For power-users that may be opening and closing liquidity positions frequently, liquidity managers can opt-in and receive their capital as ERC-6909. + + + + diff --git a/docs/contracts/v4/concepts/03-managing-position.mdx b/docs/contracts/v4/concepts/03-managing-position.mdx deleted file mode 100644 index 3a7c8b2cfd..0000000000 --- a/docs/contracts/v4/concepts/03-managing-position.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -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-hooks.mdx b/docs/contracts/v4/concepts/04-hooks.mdx new file mode 100644 index 0000000000..d354fc2726 --- /dev/null +++ b/docs/contracts/v4/concepts/04-hooks.mdx @@ -0,0 +1,59 @@ +--- +title: Hooks +--- + +Uniswap V4 introduces Hooks, a system that allows developers to customize and extend the behavior of liquidity pools. + +Hooks are external smart contracts that can be attached to individual pools. Every pool can have one hook but a hook can serve an infinite amount of pools to intercept and modify the execution flow at specific points during pool-related actions. + +## Key Concepts + +### Pool-Specific Hooks + +- Each liquidity pool in Uniswap V4 can have its own hook contract attached to it. Hooks are optional for Uniswap V4 pools. +- The hook contract is specified when creating a new pool in the `PoolManager.initialize` function. +- Having pool-specific hooks allows for fine-grained control and customization of individual pools. + +## Core Hook Functions + +Uniswap V4 provides a set of core hook functions that can be implemented by developers. Developers do not have to implement every hook, you can mix&match them to whatever your liking is. You can use one or all of them! + +- Hook contracts specify the permissions that determine which hook functions they implement, which is encoded in the address of the contract. +- The `PoolManager` uses these permissions to determine which hook functions to call for a given pool based on its Key. + +### Initialize Hooks + +- `beforeInitialize`: Called before a new pool is initialized. +- `afterInitialize`: Called after a new pool is initialized. +- These hooks allow developers to perform custom actions or validations during pool initialization, but these hooks can only be invoked once. + +### Liquidity Modification Hooks + +The liquidity modification hooks are extremely granular for security purposes. + +- `beforeAddLiquidity`: Called before liquidity is added to a pool. +- `afterAddLiquidity`: Called after liquidity is added to a pool. +- `beforeRemoveLiquidity`: Called before liquidity is removed from a pool. +- `afterRemoveLiquidity`: Called after liquidity is removed from a pool. + +### Swap Hooks + +- `beforeSwap`: Called before a swap is executed in a pool. +- `afterSwap`: Called after a swap is executed in a pool. + +### Donate Hooks + +- `beforeDonate`: Called before a donation is made to a pool. +- `afterDonate`: Called after a donation is made to a pool. +- Donate hooks provide a way to customize the behavior of token donations to liquidity providers. + +## Innovation and Potential + +The introduction of hooks in Uniswap V4 opens up a world of possibilities for developers to innovate and build new DeFi protocols. Some potential use cases include: + +- Customized AMMs with different pricing curves that xy = k. +- Yield farming and liquidity mining protocols that incentivize liquidity provision. +- Derivative and synthetic asset platforms built on top of Uniswap V4 liquidity. +- Lending hooks integrated with Uniswap V4 pools. + +As a hook developer you can easily bootstrap the codebase of an entirely new DeFi protocol through hook designs, which subsequently drives down your audit costs and allows you to develop faster. However, it's important to note that just because you made a hook, that does not mean you will get liquidity routed to your hook from the Uniswap frontend. \ No newline at end of file diff --git a/docs/contracts/v4/concepts/04-swap-tokens.mdx b/docs/contracts/v4/concepts/04-swap-tokens.mdx deleted file mode 100644 index ecc594d8cf..0000000000 --- a/docs/contracts/v4/concepts/04-swap-tokens.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -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 deleted file mode 100644 index ff779db957..0000000000 --- a/docs/contracts/v4/concepts/05-lock-mechanism.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -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/05-subscribers.mdx b/docs/contracts/v4/concepts/05-subscribers.mdx new file mode 100644 index 0000000000..44cc75f474 --- /dev/null +++ b/docs/contracts/v4/concepts/05-subscribers.mdx @@ -0,0 +1,19 @@ +--- +title: Subscribers +--- + +Subscribers, new in Uniswap v4, allow for liquidity-position owners to opt-in to a contract that receives _notifcations_. +The new design is intended to support _liquidity mining_, additional rewards given to in-range liquidity providers. Through notification logic, position owners do not need +to risk their liquidity position and its underlying assets. In Uniswap v3, _liquidity mining_ was supported by fully transferring the +liquidity position to an external contract; this old design would give the external contract full ownership and control of the liquidity position. + +When a position owner _subscribes_ to a contract, the contract will receive notifcations when: + +* The position is initially subscribed + +* The position increases or decreases its liquidity + +* The position is transferred + +* The position is unsubscribed + diff --git a/docs/contracts/v4/concepts/06-PoolManager.mdx b/docs/contracts/v4/concepts/06-PoolManager.mdx new file mode 100644 index 0000000000..660ed8ae3f --- /dev/null +++ b/docs/contracts/v4/concepts/06-PoolManager.mdx @@ -0,0 +1,87 @@ +--- +title: PoolManager +--- + +In Uniswap V3, each liquidity pool was represented by a separate smart contract deployed through the UniswapV3Factory contract. While this approach provided flexibility, it also led to increased gas costs for pool creation and multi-hop swaps. + +Uniswap V4 addresses this issue by introducing the Singleton design pattern. The PoolManager contract now serves as a single entry point for all liquidity pools. Instead of deploying separate contracts for each pool, the pool state and logic are encapsulated within the PoolManager itself. + +# Purpose + +The primary purpose of the `PoolManager` is to: + +- Efficiently manage liquidity pools +- Facilitate token swaps +- Reduce gas costs compared to the factory-based approach in Uniswap V3 +- Enable extensibility through hooks + +# Architecture + +## Singleton Design + +- Uniswap V4 uses a Singleton design pattern for the `PoolManager` +- All pool state and logic are encapsulated within the `PoolManager` contract + +## Locking Mechanism + +- The `PoolManager` uses a locking mechanism to allow for _flash accounting_ (also known as deferred balance accounting) +- When unlocked, the calling contract can perform various operations and zero-out outstanding balances before returning control to the `PoolManager` for final solvency checks + +## Pool State + +- The `Pool.State` struct contains information such as: + - Current price + - Liquidity + - Tick bitmap + - Fee growth + - Position information + +## Libraries + +- The pool logic is implemented using Solidity libraries to keep the `PoolManager` contract modular and gas-efficient +- These libraries are: + - `Pool`: Contains core pool functionality, such as swaps and liquidity management + - `Hooks`: Handles the execution of hook functions + - `Position`: Manages liquidity positions within a pool + +# Core Functionality + +## Pool Creation + +- New pools are created by calling the `initialize` function on the `PoolManager` +- The pool creator specifies the token pair, fee tier, tick spacing, and optional hook contract address +- The `PoolManager` initializes the pool state and associates it with a unique `PoolId` + +## Swaps + +- Swaps are initiated through the `swap` function on the `PoolManager`, typically via a swap router contract +- The `PoolManager` executes the following steps: + 1. Checks if the pool is valid and initialized + 2. Executes the `beforeSwap` hook, if applicable + 3. Performs the actual swap, updating the pool state and charging fees + 4. Executes the `afterSwap` hook, if applicable + 5. Calculates the net token amounts owed to the user and the pool, represented by the `BalanceDelta` struct +- Swaps utilize flash accounting, where tokens are moved into the `PoolManager`, and only the final output tokens are withdrawn + +## Liquidity Management + +- Liquidity providers can add or remove liquidity using the `modifyLiquidity` function on the `PoolManager`. However, you wouldn't call this directly from your application, you would call this from a periphery contract to handle the locking & unlocking a particular pool. +- The `PoolManager` executes the following steps: + 1. Checks if the pool is valid and initialized + 2. Determines if the modification is an addition or removal of liquidity + 3. Executes the appropriate `beforeAddLiquidity` or `beforeRemoveLiquidity` hook, if applicable + 4. Performs the actual liquidity modification and updates the pool state + 5. Emits the `ModifyLiquidity` event + 6. Executes the appropriate `afterAddLiquidity` or `afterRemoveLiquidity` hook, if applicable + 7. Calculates the balance delta and returns it to the caller + +## Flash Accounting + +- The `PoolManager` employs flash accounting to reduce gas costs and simplify multi-hop swaps +- Tokens are moved into the `PoolManager` contract, and all subsequent actions are performed within the contract's context +- Only the final output tokens are withdrawn from the `PoolManager` at the end of the transaction + +# Transient Storage + +- The `PoolManager` utilizes transient storage (EIP-1153) to store temporary data during complex operations +- Transient storage reduces gas costs by avoiding regular storage operations for data only needed within a single transaction \ No newline at end of file diff --git a/docs/contracts/v4/concepts/07-deployment.mdx b/docs/contracts/v4/concepts/07-deployment.mdx deleted file mode 100644 index efa725cd8a..0000000000 --- a/docs/contracts/v4/concepts/07-deployment.mdx +++ /dev/null @@ -1,196 +0,0 @@ ---- -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/07-dynamic-fees.mdx b/docs/contracts/v4/concepts/07-dynamic-fees.mdx new file mode 100644 index 0000000000..842eb2172d --- /dev/null +++ b/docs/contracts/v4/concepts/07-dynamic-fees.mdx @@ -0,0 +1,61 @@ +--- +title: Dynamic Fees +--- + +Uniswap v4 introduces dynamic fees, allowing for flexible and responsive fee structures managed through hooks. This feature enables pools to adapt fees to changing market conditions, potentially improving liquidity provider profitability and overall market efficiency. + +# What are Dynamic Fees? + +Dynamic fees in Uniswap v4 are a specific type of swap fee paid by swappers that directly accrue to liquidity providers. These fees are distinct from protocol fees and hook fees (Optional fees that can be implemented by custom hooks), and represent a significant advancement over the fee structures in previous Uniswap versions. + +Unlike the static fee tiers in Uniswap v3 (0.05%, 0.30%, 1.0%) or the single fee in v2, dynamic fees in v4 offer much more flexibility. Dynamic fees can: + +- Adjust in real-time based on various market conditions +- Change on a per-swap basis +- Allow for any fee percentage (e.g., 4.9 bips, 10 bips) +- Be updated at various intervals (yearly, per block, or per transaction) + +This dynamic nature allows for more efficient fee pricing, potentially benefiting both liquidity providers and traders by adapting to current market conditions. By allowing fees to fluctuate based on market dynamics, Uniswap v4 aims to optimize liquidity provision and trading across a wide range of market scenarios. + +# Motivation and Benefits of Dynamic Fees + +1. **Improved Pricing of Volatility:** Adapt fees to market volatility, similar to traditional exchanges adjusting bid-ask spreads. +2. **Order Flow Discrimination:** Price different types of trades (e.g., arbitrage vs. uninformed) more accurately. +3. **Improved Market Efficiency and Stability:** Fees can adjust to reflect real-time market conditions, optimizing for both liquidity providers and traders. Dynamic fees could help dampen extreme market movements by adjusting incentives in real-time. +4. **Enhanced Capital Efficiency and Liquidity Provider Returns:** By optimizing fees, pools can attract more liquidity and facilitate more efficient trading. More accurate fee pricing could lead to better returns for liquidity providers, potentially attracting more capital to pools. +5. **Better Risk Management:** During high volatility, fees can increase to protect liquidity providers from impermanent loss. +6. **Customizable Strategies:** Enable complex fee strategies for specific token pairs or market segments. + +# Dynamic Fees Use Cases + +1. **Volatility-Based Fees:** Adjust fees based on the historical or realized volatility of the asset pair. +2. **Volume-Based Fees:** Lower fees during high-volume periods to attract more trades, and increase fees during low-volume periods to compensate liquidity providers. +3. **Time-Based Fees:** Implement different fee structures for different times of day or days of the week, based on historical trading patterns. +4. **Market Depth-Based Fees:** Adjust fees based on the current liquidity depth in the pool. +5. **Cross-Pool Arbitrage Mitigation:** Dynamically adjust fees to discourage harmful arbitrage between different pools or exchanges. +6. **Gas Price-Responsive Fees:** Adjust fees based on network congestion and gas prices to ensure profitability for liquidity providers. +7. **Event-Driven Fees:** Implement special fee structures during significant market events or token-specific occurrences. +8. **Lookback approach:** Set the fee to match the most profitable fee tier of external pools with the same asset pair over a recent period. +9. **Price oracle approach:** Use an external price oracle to determine the correct asset price and adjust fees based on how trades move the pool price relative to this external price. +10. **Price momentum approach:** Analyze recent price history and asymmetrically adjust fees based on trade direction. +11. **Asset composition approach:** Lower fees for trades that balance the pool and higher fees for trades that imbalance it. +12. **Transaction-source based approach:** Provide lower fees for transactions routed through certain aggregators or sources less likely to be arbitrage trades. + +# Dynamic Fees Mechanism + +In Uniswap v4, the dynamic fee capability of a pool is determined at pool creation and is immutable. This means that whether a pool uses dynamic fees or not is set when the pool is initially created and cannot be changed afterwards. For pools that do use dynamic fees, Uniswap v4 supports two primary methods for updating the fee: + +1. **Periodic Updates via PoolManager:** Fees can be updated by calling the `updateDynamicLPFee` function on the PoolManager contract at specified intervals. +2. **Per-Swap Updates via beforeSwap Hook:** Fees can be dynamically set for each swap by returning the fee from the `beforeSwap` hook. This allows hooks to override the LP fee for each swap in dynamic fee pools. + +These methods offer flexibility in implementing various fee strategies. For more detailed information on implementing these methods, please refer to our [Dynamic Fees Implementation Guide](https://uniswap-docs-staging.vercel.app/documentation/featured-guides/hooks/v4/guides-for-solidity-contracts/dynamic-fee-pools). + +# Considerations and Best Practices + +- The optimal fee depends on at least two factors: **asset volatility** and **volume of uninformed flow.** +- For volatile pairs in systems like Uniswap v3, which don't discriminate between flows, low fee-tier pools are only sensible when uninformed flow is large and asset volatility is relatively low. +- Performance implications of frequent fee updates should be carefully considered. +- Security measures should be implemented to prevent manipulation of fee-setting mechanisms. +- Balance responsiveness with gas costs to optimize for both performance and cost-effectiveness. + +For more detailed implementation guidance and best practices, refer to our [Dynamic Fees Implementation Guide](https://uniswap-docs-staging.vercel.app/documentation/featured-guides/hooks/v4/guides-for-solidity-contracts/dynamic-fee-pools). \ No newline at end of file diff --git a/docs/contracts/v4/concepts/_category_.json b/docs/contracts/v4/concepts/_category_.json index 74b831d116..6e81e196df 100644 --- a/docs/contracts/v4/concepts/_category_.json +++ b/docs/contracts/v4/concepts/_category_.json @@ -1,5 +1,5 @@ { "label": "Concepts", - "position": 2, - "collapsed": false + "position": 3, + "collapsed": true } diff --git a/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png b/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png deleted file mode 100644 index 84d466c9a0..0000000000 Binary files a/docs/contracts/v4/concepts/images/01_Pool_Initialization/HighLevelArchitecture.png and /dev/null 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 deleted file mode 100644 index 3001b623fc..0000000000 Binary files a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_detailed_architecture.png and /dev/null 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 deleted file mode 100644 index 2bd471a1bd..0000000000 Binary files a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v3_high_level_architecture.png and /dev/null 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 deleted file mode 100644 index 6e93648525..0000000000 Binary files a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_detailed_architecture.png and /dev/null 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 deleted file mode 100644 index acbeb32b05..0000000000 Binary files a/docs/contracts/v4/concepts/images/01_Pool_Initialization/v4_high_level_architecture.png and /dev/null 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 deleted file mode 100644 index 65755db541..0000000000 Binary files a/docs/contracts/v4/concepts/images/02_Managing_Position/PriceAndTicks.png and /dev/null 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 deleted file mode 100644 index 87fd7764ee..0000000000 Binary files a/docs/contracts/v4/concepts/images/02_Managing_Position/SqrtPriceX96.png and /dev/null 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 deleted file mode 100644 index f4fb57d763..0000000000 Binary files a/docs/contracts/v4/concepts/images/02_Managing_Position/sqrtPriceX96_to_tick.png and /dev/null 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 deleted file mode 100644 index b3465bd19f..0000000000 Binary files a/docs/contracts/v4/concepts/images/03_fee_calculation/FeeCalculation.png and /dev/null 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 deleted file mode 100644 index 3f09b44a7b..0000000000 Binary files a/docs/contracts/v4/concepts/images/05_locking_mechanism/LockingMechanism_excali.png and /dev/null 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 deleted file mode 100644 index e502cd1720..0000000000 Binary files a/docs/contracts/v4/concepts/images/05_locking_mechanism/locking_mechanism.png and /dev/null differ diff --git a/docs/contracts/v4/concepts/images/Liquidity1.png b/docs/contracts/v4/concepts/images/Liquidity1.png deleted file mode 100644 index 719d0af81a..0000000000 Binary files a/docs/contracts/v4/concepts/images/Liquidity1.png and /dev/null differ diff --git a/docs/contracts/v4/deployments.mdx b/docs/contracts/v4/deployments.mdx new file mode 100644 index 0000000000..35abc312f1 --- /dev/null +++ b/docs/contracts/v4/deployments.mdx @@ -0,0 +1,40 @@ +--- +id: deployments +title: Deployments +sidebar_position: 1.1 +--- + +# Uniswap v4 Deployments + +Uniswap v4 is NOT live on a production environment yet + +However, there are deployments to testnet environments. Please be aware the deployments may not +be up to date with the latest changes. + +For the latest deployments, please see the [foundry artifacts](https://github.com/Uniswap/v4-periphery/tree/main/broadcast) + +## Sepolia: 11155111 + +| Contract | Address | +|--------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| [PoolManager](https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol) | [`0xf242ce588b030d0895c51c0730f2368680f80644`](https://sepolia.etherscan.io/address/0xf242ce588b030d0895c51c0730f2368680f80644#code) | +| [Universal Router](https://github.com/Uniswap/universal-router/blob/dev/contracts/UniversalRouter.sol) | [`0x0000000000000000000000000000000000000000`](https://sepolia.etherscan.io/address/0x0000000000000000000000000000000000000000#code) | +| [PositionManager](https://github.com/Uniswap/v4-periphery/blob/main/src/PositionManager.sol) | [`0x0000000000000000000000000000000000000000`](https://sepolia.etherscan.io/address/0x0000000000000000000000000000000000000000#code) | +| [StateView](https://github.com/Uniswap/v4-periphery/blob/main/src/lens/StateView.sol) | [`0xc7a3b85d43ff66ad98a895de0eae4b9e24c932d7`](https://sepolia.etherscan.io/address/0xc7a3b85d43ff66ad98a895de0eae4b9e24c932d7#code) | +| [Quoter](https://github.com/Uniswap/v4-periphery/blob/main/src/lens/Quoter.sol) | [`0x0000000000000000000000000000000000000000`](https://sepolia.etherscan.io/address/0x0000000000000000000000000000000000000000#code) | +| [PoolSwapTest](https://github.com/Uniswap/v4-core/blob/main/src/test/PoolSwapTest.sol) | [`0x841b5a0b3dbc473c8a057e2391014aa4c4751351`](https://sepolia.etherscan.io/address/0x841b5a0b3dbc473c8a057e2391014aa4c4751351#code) | +| [PoolModifyLiquidityTest](https://github.com/Uniswap/v4-core/blob/main/src/test/PoolModifyLiquidityTest.sol) | [`0x39bf2eff94201cfaa471932655404f63315147a4`](https://sepolia.etherscan.io/address/0x39bf2eff94201cfaa471932655404f63315147a4#code) | + +(@TODO: update universal router link hehe) + +## Base Sepolia: 84532 + +| Contract | Address | +|--------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| [PoolManager](https://github.com/Uniswap/v4-core/blob/main/src/PoolManager.sol) | [`0xf242ce588b030d0895c51c0730f2368680f80644`](https://sepolia.basescan.org/address/0xf242ce588b030d0895c51c0730f2368680f80644#code) | +| [Universal Router](https://github.com/Uniswap/universal-router/blob/dev/contracts/UniversalRouter.sol) | [`0x927038542746c06F1b2F7F550a3c90AEBdDa4E85`](https://sepolia.basescan.org/address/0x927038542746c06F1b2F7F550a3c90AEBdDa4E85#code) | +| [PositionManager](https://github.com/Uniswap/v4-periphery/blob/main/src/PositionManager.sol) | [`0xa2f16f0bb5dea7c9a6675ec88193471dee805e6e`](https://sepolia.basescan.org/address/0xa2f16f0bb5dea7c9a6675ec88193471dee805e6e#code) | +| [StateView](https://github.com/Uniswap/v4-periphery/blob/main/src/lens/StateView.sol) | [`0xfb3e0c6f74eb1a21cc1da29aec80d2dfe6c9a317`](https://sepolia.basescan.org/address/0xfb3e0c6f74eb1a21cc1da29aec80d2dfe6c9a317#code) | +| [Quoter](https://github.com/Uniswap/v4-periphery/blob/main/src/lens/Quoter.sol) | [`0xf3a39c86dbd13c45365e57fb90fe413371f65af8`](https://sepolia.basescan.org/address/0xf3a39c86dbd13c45365e57fb90fe413371f65af8#code) | +| [PoolSwapTest](https://github.com/Uniswap/v4-core/blob/main/src/test/PoolSwapTest.sol) | [`0xff34e285f8ed393e366046153e3c16484a4dd674`](https://sepolia.basescan.org/address/0xff34e285f8ed393e366046153e3c16484a4dd674#code) | +| [PoolModifyLiquidityTest](https://github.com/Uniswap/v4-core/blob/main/src/test/PoolModifyLiquidityTest.sol) | [`0x841b5a0b3dbc473c8a057e2391014aa4c4751351`](https://sepolia.basescan.org/address/0x841b5a0b3dbc473c8a057e2391014aa4c4751351#code) | \ No newline at end of file diff --git a/docs/contracts/v4/first-hook/01-building-you-hook.mdx b/docs/contracts/v4/first-hook/01-building-you-hook.mdx deleted file mode 100644 index dbb282c588..0000000000 --- a/docs/contracts/v4/first-hook/01-building-you-hook.mdx +++ /dev/null @@ -1,125 +0,0 @@ ---- -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 deleted file mode 100644 index a63e5acfac..0000000000 --- a/docs/contracts/v4/first-hook/02-hook-testing.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -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 deleted file mode 100644 index 1aa83aa6ce..0000000000 --- a/docs/contracts/v4/first-hook/03-hook-deployment.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -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 deleted file mode 100644 index 2b5931eddd..0000000000 --- a/docs/contracts/v4/first-hook/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "My First Hook", - "position": 3, - "collapsed": false -} diff --git a/docs/contracts/v4/guides/01-create-pool.mdx b/docs/contracts/v4/guides/01-create-pool.mdx new file mode 100644 index 0000000000..4ce142c22b --- /dev/null +++ b/docs/contracts/v4/guides/01-create-pool.mdx @@ -0,0 +1,7 @@ +--- +title: Create Pool +--- + +## 🚧 Under construction 🚧 + +Please see https://www.v4-by-example.org/initialize diff --git a/docs/contracts/v4/guides/02-manage-liquidity/00-setup-liquidity.mdx b/docs/contracts/v4/guides/02-manage-liquidity/00-setup-liquidity.mdx new file mode 100644 index 0000000000..8ef5b891bc --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/00-setup-liquidity.mdx @@ -0,0 +1,29 @@ +--- +title: Setup +--- + +# Setup + +For users looking to interact with the canonical Uniswap v4 `PositionManager`, _v4-periphery_ is a required dependency + +Currently, developing with Uniswap v4 _requires [foundry](https://book.getfoundry.sh)_ + +## Quickstart + +_Use [v4-template](https://github.com/new?template_name=v4-template&template_owner=uniswapfoundation)_, which has pre-configured dependencies and tests for Uniswap v4 + +Clone the repository made from _v4-template_ +```bash +git clone https://github.com// +``` + +Install dependencies +```bash +forge install +``` + +--- + +## Manual + +(TODO: Add instructions for manual setup) \ No newline at end of file diff --git a/docs/contracts/v4/guides/02-manage-liquidity/01-mint-position.mdx b/docs/contracts/v4/guides/02-manage-liquidity/01-mint-position.mdx new file mode 100644 index 0000000000..9b771c6288 --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/01-mint-position.mdx @@ -0,0 +1,90 @@ +--- +title: Mint Position +--- + +Similar to Uniswap v3, liquidity positions are minted as ERC-721 tokens and depend on a *periphery* contract. +v4's `PositionManager` contract will facilitate liquidity management + +### Context + +Please note that `PositionManager` is a command-based contract, where integrators will be encoding commands and their corresponding +parameters. + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +Below is a step-by-step guide for minting a v4 liquidity position, in *solidity* + +### 1. Import and define `IPositionManager` + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; + +// inside a contract, test, or foundry script: +IPositionManager posm = IPositionManager(
); +``` + +### 2. Encode Actions + +To mint a position, two actions are required: + +* mint operation - the creation of the liquidity position +* settle pair - the two tokens to be paid by msg.sender + +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; + +bytes memory actions = abi.encodePacked(Actions.MINT_POSITION, Actions.SETTLE_PAIR); +``` + +### 3. Encode Parameters + +```solidity +bytes[] memory params = new bytes[](2); +``` + +The `MINT_POSITION` action requires the following parameters: + +| Parameter | Type | Description | +|--------------|-----------|----------------------------------------------------------------| +| `poolKey` | _PoolKey_ | where the liquidity will be added to | +| `tickLower` | _int24_ | the lower tick boundary of the position | +| `tickUpper` | _int24_ | the upper tick boundary of the position | +| `liquidity` | _uint256_ | the amount of liquidity units to mint | +| `amount0Max` | _uint128_ | the maximum amount of currency0 msg.sender is willing to pay | +| `amount1Max` | _uint128_ | the maximum amount of currency1 msg.sender is willing to pay | +| `recipient` | _address_ | the address that will receive the liquidity position (ERC-721) | +| `hookData` | _bytes_ | arbitrary data that will be forwarded to hook functions | + +```solidity +params[0] = abi.encode(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData); +``` + +The `SETTLE_PAIR` action requires the following parameters: + +* `currency0` - _Currency_, one of the tokens to be paid by msg.sender +* `currency1` - _Currency_, the other token to be paid by msg.sender + +```solidity +params[1] = abi.encode(currency0, currency1); +``` + +### 4. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()` + +```solidity +uint256 deadline = block.timestamp + 60; + +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` + +## Additional notes: + +* To obtain balance changes, callers should read token balances before and after the `.modifyLiquidities()` call \ No newline at end of file diff --git a/docs/contracts/v4/guides/02-manage-liquidity/02-increase-liquidity.mdx b/docs/contracts/v4/guides/02-manage-liquidity/02-increase-liquidity.mdx new file mode 100644 index 0000000000..f3023a0d8a --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/02-increase-liquidity.mdx @@ -0,0 +1,138 @@ +--- +title: Increase Liquidity +--- + +### Context + +Please note that `PositionManager` is a command-based contract, where integrators will be encoding commands and their corresponding +parameters. + +Increasing liquidity assumes the position already exists and the user wants to add more tokens to the position. + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +Below is a step-by-step guide for increasing a position's liquidity, in *solidity*. + +### 1. Import and define `IPositionManager` + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; + +// inside a contract, test, or foundry script: +IPositionManager posm = IPositionManager(
); +``` + +### 2. Encode Actions + +To increase a position's liquidity, the first action must be: + +* _increase_ operation - the addition of liquidity to an existing position. + +For _delta resolving_ operations, developers may need to choose between `SETTLE_PAIR`, `CLOSE_CURRENCY`, or `CLEAR_OR_TAKE` actions. + +> In Uniswap v4, fee revenue is automatically credited to a position on increasing liquidity + +> There are some cases, where the fee revenue can entirely "pay" for a liquidity increase, and remainder tokens need to be collected + +If increasing the liquidity requires the transfer of both tokens: + +* _settle pair_ - pays a pair of tokens, to increase liquidity + +Otherwise: + +* _close currency_ - automatically determines if a currency should be settled or taken. +* OR _clear or take_ - if the token amount to-be-collected is below a threshold, opt to forfeit the dust. Otherwise, claim the tokens + +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +``` + +If both tokens need to be sent: +```solidity +bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.SETTLE_PAIR)); +``` + +If converting fees to liquidity, and expect excess fees to be collected +```solidity +bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.CLOSE_CURRENCY), uint8(Actions.CLOSE_CURRENCY)); +``` + +If converting fees to liquidity, forfeiting dust: +```solidity +bytes memory actions = abi.encodePacked(uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.CLEAR_OR_TAKE), uint8(Actions.CLEAR_OR_TAKE)); +``` + +### 3. Encoded Parameters + +When settling pair: + +```solidity +bytes[] memory params = new bytes[](2); +``` + +Otherwise: + +```solidity +bytes[] memory params = new bytes[](3); +``` + +The `INCREASE_LIQUIDITY` action requires the following parameters: + +| Parameter | Type | Description | +|--------------|-----------|-------------------------------------------------------------------------------| +| `tokenId` | _uint256_ | position identifier | +| `liquidity` | _uint256_ | the amount of liquidity to add | +| `amount0Max` | _uint128_ | the maximum amount of currency0 liquidity msg.sender is willing to pay | +| `amount1Max` | _uint128_ | the maximum amount of currency1 liquidity msg.sender is willing to pay | +| `hookData` | _bytes_ | arbitrary data that will be forwarded to hook functions | + +```solidity +params[0] = abi.encode(tokenId, liquidity, amount0Max, amount1Max, hookData); +``` + +The `SETTLE_PAIR` action requires the following parameters: + +* `currency0` - _Currency_, one of the tokens to be paid by msg.sender +* `currency1` - _Currency_, the other token to be paid by msg.sender + +In the above case, the parameter encoding is: + +```solidity +params[1] = abi.encode(currency0, currency1); +``` + +The `CLOSE_CURRENCY` action requires only one `currency` parameter +and the encoding is: + +```solidity +params[1] = abi.encode(currency0) +params[2] = abi.encode(currency1) +``` + +The `CLEAR_OR_TAKE` action requires one `currency` and: + +* `amountMax` - _uint256_, the maximum threshold to concede dust, +otherwise taking the dust. + +In this case, the parameter encoding is: + +```solidity +params[1] = abi.encode(currency0, amount0Max); +params[2] = abi.encode(currency1, amount1Max); +``` + +### 4. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()`. + +```solidity +uint256 deadline = block.timestamp + 60; +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/02-manage-liquidity/03-decrease-liquidity.mdx b/docs/contracts/v4/guides/02-manage-liquidity/03-decrease-liquidity.mdx new file mode 100644 index 0000000000..2821266d49 --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/03-decrease-liquidity.mdx @@ -0,0 +1,123 @@ +--- +title: Decrease Liquidity +--- + +### Context + +Please note that `PositionManager` is a command-based contract, where integrators will be encoding commands and their corresponding +parameters. + +Decreasing liquidity assumes the position already exists and the user wants to remove tokens from the position. + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +Below is a step-by-step guide for decreasing a position's liquidity, in *solidity*. + +### 1. Import and define `IPositionManager` + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; + +// inside a contract, test, or foundry script: +IPositionManager posm = IPositionManager(
); +``` + +### 2. Encode Actions + +To decrease a position's liquidity, the first action must be: + +* _decrease_ operation - the subtraction of liquidity to an existing position. + +For _delta resolving_ operations, developers may need to choose between `TAKE_PAIR`, `CLOSE_CURRENCY`, or `CLEAR_OR_TAKE` actions. + +> In Uniswap v4, fee revenue is automatically debited to a position on decreasing liquidity + +If decreasing the liquidity requires the transfer of both tokens: + +* _take pair_ - receives a pair of tokens, to decrease liquidity + +Otherwise: + +* _clear or take_ - if the token amount to-be-collected is below a threshold, opt to forfeit the dust. Otherwise, claim the tokens + +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +``` + +If both tokens need to be sent: +```solidity +bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)); +``` + +If converting fees to liquidity, forfeiting dust: +```solidity +bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.CLEAR_OR_TAKE), uint8(Actions.CLEAR_OR_TAKE)); +``` + +### 3. Encoded Parameters + +When taking pair: + +```solidity +bytes[] memory params = new bytes[](2); +``` + +Otherwise: + +```solidity +bytes[] memory params = new bytes[](3); +``` + +The `DECREASE_LIQUIDITY` action requires the following parameters: + +| Parameter | Type | Description | +|--------------|-----------|-------------------------------------------------------------------------------| +| `tokenId` | _uint256_ | position identifier | +| `liquidity` | _uint256_ | the amount of liquidity to remove | +| `amount0Min` | _uint128_ | the minimum amount of currency0 liquidity msg.sender is willing to receive | +| `amount1Min` | _uint128_ | the minimum amount of currency1 liquidity msg.sender is willing to receive | +| `hookData` | _bytes_ | arbitrary data that will be forwarded to hook functions | + +```solidity +params[0] = abi.encode(tokenId, liquidity, amount0Min, amount1Min, hookData); +``` + +The `TAKE_PAIR` action requires the following parameters: + +* `currency0` - _Currency_, one of the tokens to be received +* `currency1` - _Currency_, the other token to be received +* `recipient` - _Recipient_, the recipient to receive the tokens + +In the above case, the parameter encoding is: + +```solidity +params[1] = abi.encode(currency0, currency1, recipient); +``` + +The `CLEAR_OR_TAKE` action requires one `currency` and: + +* `amountMax` - _uint256_, the maximum threshold to concede dust, +otherwise taking the dust. + +In this case, the parameter encoding is: + +```solidity +params[1] = abi.encode(currency0, amount0Max); +params[2] = abi.encode(currency1, amount1Max); +``` + +### 4. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()`. + +```solidity +uint256 deadline = block.timestamp + 60; +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/02-manage-liquidity/04-collect.mdx b/docs/contracts/v4/guides/02-manage-liquidity/04-collect.mdx new file mode 100644 index 0000000000..02a38b7d75 --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/04-collect.mdx @@ -0,0 +1,86 @@ +--- +title: Collect Fees +--- + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +In order to collect fees, the integrator must execute encoded actions +using the `PositionManager` contract. **Note** that there is no +`COLLECT` command, instead developers must decrease liquidity with a zero +liquidity change. + +### 1. Import and define `IPositionManager` + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +// inside a contract, test, or foundry script: +IPositionManager posm = IPositionManager(
); +``` + +### 2. Encode actions + +To collect fees, the following operations are required: +* decrease liquidity - collect fees from the core contract +* take pair - transfer the fee revenue, as both tokens, to a recipient + +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +bytes memory actions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)); +``` + +### 3. Encode Parameters + +```solidity +bytes[] memory params = new bytes[](2); +``` + +The `DECREASE_LIQUIDITY` action requires the following parameters: + +| Parameter | Type | Description | +|--------------|-----------|-------------------------------------------------------------------------------| +| `tokenId` | _uint256_ | position identifier | +| `liquidity` | _uint256_ | the amount of liquidity to withdraw | +| `amount0Min` | _uint128_ | the minimum amount of currency0 liquidity msg.sender is expecting to get back | +| `amount1Min` | _uint128_ | the minimum amount of currency1 liquidity msg.sender is expecting to get back | +| `hookData` | _bytes_ | arbitrary data that will be forwarded to hook functions | + +**Note** that in order to collect fees we will default `liquidity`, `amount0Min` and `amount1Min` to 0. +Because fee collection can not be manipulated in a front-run attack, it is safe to set the slippage +values `amount0Min, amount1Min` to `0`. + +```solidity +/// @dev collecting fees is achieved with liquidity=0, the second parameter +params[0] = abi.encode(tokenId, 0, 0, 0, hookData); +``` + +The `TAKE_PAIR` action requires the following parameters: + +* `currency0` - _Currency_, one of the tokens to be paid by msg.sender +* `currency1` - _Currency_, the other token to be paid by msg.sender +* `recipient` - _address_, destination of the fee revenue for both tokens + +```solidity +params[1] = abi.encode(currency0, currency1, recipient); +``` + +### 4. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()`. + +```solidity +uint256 deadline = block.timestamp + 60; +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` + +## Additional notes: + +* To obtain the amount of fees received, callers should read +token balances before and after the `.modifyLiquidities()` call. + diff --git a/docs/contracts/v4/guides/02-manage-liquidity/05-burn-liquidity.mdx b/docs/contracts/v4/guides/02-manage-liquidity/05-burn-liquidity.mdx new file mode 100644 index 0000000000..66728f1b55 --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/05-burn-liquidity.mdx @@ -0,0 +1,72 @@ +--- +title: Burn Position +--- + +### Context + +To liquidate a position, the _burn_ functionality can be invoked. +The funds in the position will be withdrawn and +all the information of the underlying token will be cleared. +Burning the position is a cost effective way to +exit as a liquidity provider. + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +Below is a step-by-step guide to burn a position. + +### 1. Import and define `IPositionManager` + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; + +// inside a contract, test, or foundry script: +IPositionManager posm = IPositionManager(
); +``` + +### 2. Encode Actions + +To burn a position, one action is required: + +* burn operation - clears position entirely, withdrawing funds + +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; + +bytes memory actions = abi.encodePacked(Actions.BURN_POSITION); +``` + +### 3. Encode Parameters + +```solidity +bytes[] memory params = new bytes[](1); +``` + +The `BURN_POSITION` action requires the following parameters: + +| Parameter | Type | Description | +|--------------|-----------|-------------------------------------------------------------------------------| +| `tokenId` | _uint256_ | position identifier | +| `amount0Min` | _uint128_ | the minimum amount of currency0 liquidity msg.sender is expecting to get back | +| `amount1Min` | _uint128_ | the minimum amount of currency1 liquidity msg.sender is expecting to get back | +| `hookData` | _bytes_ | arbitrary data that will be forwarded to hook functions | + +```solidity +params[0] = abi.encode(tokenId, amount0Min, amount1Min, hookData); +``` + +### 4. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()` + +```solidity +uint256 deadline = block.timestamp + 60; + +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/02-manage-liquidity/06-batch-liquidity.mdx b/docs/contracts/v4/guides/02-manage-liquidity/06-batch-liquidity.mdx new file mode 100644 index 0000000000..2b1c7da78d --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/06-batch-liquidity.mdx @@ -0,0 +1,79 @@ +--- +title: Batch Modify +--- + +### Context + +As seen in previous guides, `PositionManager` is a command-based contract. This design is conducive to +batching complex liquidity operations. For example, developers can encode efficient logic to move +liquidity between two positions on entirely different Pools. + +### Setup + +See the [setup guide](./00-setup-liquidity.mdx) + +# Guide + +Below is a general reference guide for batch-operating on multiple liquidity positions, in *solidity*. +This guide does _not_ focus on a specific batch sequence, and is intended to be a general guide for `PositionManager`'s command-based interface. + +### 1. Encoded Actions + +Actions are divided into two types: _liquidity-operations_ and _delta-resolving_. + +* _liquidity-operations_ - actions which that incur a *balance-change*, a change in the pool's liquidity +* _delta-resolving_ - actions which facilitate token transfers, such as _settling_ and _taking_ + +The _ordering_ of `actions` determines the sequence of operations. The minimum number of actions is roughly two actions; and the maximum is limited by block gas limit. +Additionally, _liquidity-operations_ do not have to happen prior to _delta-resolving_ actions. Developers can mix / alternate between +the two types of actions. + +> **However** is good practice to perform _liquidity-operations_ before _delta-resolving_ actions. Minimizing token transfers +and leveraging [_flash accounting_](../../concepts/02-flash-accounting.mdx) is more gas efficient + +Example: `Action.Y` happens after `Action.X` but before `Action.Z` +```solidity +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; + +bytes memory actions = abi.encodePacked(uint8(Actions.X), uint8(Actions.Y), uint8(Actions.Z), ...); +``` + +**A Note on Special Actions**: + +`PositionManager` supports a few _delta-resolving_ actions beyond the standard `SETTLE` and `TAKE` actions + +* `CLOSE_CURRENCY` - automatically determines if a currency should be settled (paid) or taken. Used for cases where callers may not know the final delta +* `CLEAR_OR_TAKE`- forfeit tokens if the amount is below a specified threshold, otherwise take the tokens. Used for cases where callers may expect to produce dust +* `SWEEP` - return any excess token balances to a recipient. Used for cases where callers may conversatively overpay tokens + +### 2. Encoded Parameters + +Each action has its own parameters to encode. Generally: + +* _liquidity-operations_ - encode tokenIds, liquidity amounts, and slippage + +* _delta-resolving_ - encode currencies, amounts, and recipients + +Because actions are ordered, the parameters "zip" with their corresponding actions. The second parameter corresponds to the second action. +Every action has its own encoded parameters + +```solidity +bytes[] memory params = new bytes[](3); + +params[0] = abi.encode(...); // parameters for the first action +params[1] = abi.encode(...); // parameters for the second action +params[2] = abi.encode(...); // parameters for the third action +``` + +### 3. Submit Call + +The entrypoint for all liquidity operations is `modifyLiquidities()` + +```solidity +uint256 deadline = block.timestamp + 60; + +posm.modifyLiquidities( + abi.encode(actions, params), + deadline +); +``` diff --git a/docs/contracts/v4/guides/02-manage-liquidity/_category_.json b/docs/contracts/v4/guides/02-manage-liquidity/_category_.json new file mode 100644 index 0000000000..a0b9fd05b4 --- /dev/null +++ b/docs/contracts/v4/guides/02-manage-liquidity/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Manage Liquidity", + "collapsed": true +} diff --git a/docs/contracts/v4/guides/03-swap.mdx b/docs/contracts/v4/guides/03-swap.mdx new file mode 100644 index 0000000000..d7bc6f0202 --- /dev/null +++ b/docs/contracts/v4/guides/03-swap.mdx @@ -0,0 +1,7 @@ +--- +title: Swap +--- + +## 🚧 Under construction 🚧 + +Please see https://www.v4-by-example.org/swap \ No newline at end of file diff --git a/docs/contracts/v4/guides/04-hooks/00-setup.mdx b/docs/contracts/v4/guides/04-hooks/00-setup.mdx new file mode 100644 index 0000000000..f350edcba7 --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/00-setup.mdx @@ -0,0 +1,225 @@ +--- +title: Set Up Local Environment +--- + +Before writing the hook let's first have a local environment properly configured e.g. deploying pool manager, utility routers and test tokens. + +At the end of this guide a development environment will be set up to be used to build the rest of the examples in the Guides section of the docs. + +To get started as quickly as possible for building Uniswap v4 hooks, there is a `Quick Start` section below to clone a boilerplate and get building. To start from scratch and learn the underlying concepts, jump to the `Start from Scratch` section. + +# Quick Start + +The Uniswap [v4-template repo](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 +``` + +Then hop to the [Local Node via Anvil](#local-node-via-anvil) to complete the set up and start developing. + +--- + +# Start from Scratch + +In the following sections, let's walk through the steps to create the same environment set up as the boilerplate from scratch and learn the underlying concepts. + +## Setting up Foundry + +First thing is to set up a new Foundry project. + +If there is no Foundry installed - follow the [Foundry Book](https://book.getfoundry.sh/getting-started/installation) for installation. + +Once Foundry is setup, initialize a new project: + +```bash +forge init counter-hook +cd counter-hook +``` + +Then install the Uniswap `v4-core` and `v4-periphery` contracts as dependencies: + +```bash +forge install Uniswap/v4-core && forge install Uniswap/v4-periphery +``` + +Next, set up the remappings so that the shorthand syntax for importing contracts from the dependencies work nicely: + +```bash +forge remappings > remappings.txt +``` + +> If there is something wrong with the inferred remappings, please replace with the following in `remappings.txt`: + +``` +@uniswap/v4-core/=lib/v4-core/ +forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/ +forge-std/=lib/v4-core/lib/forge-std/src/ +permit2/=lib/v4-periphery/lib/permit2/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/ +v4-periphery/=lib/v4-periphery/ +``` + +After that, remove the default Counter contract and its associated test and script file that Foundry initially set up. To do that, either manually delete those files, or just run the following: + +```bash +rm ./**/Counter*.sol +``` + +Finally, since v4 uses transient storage which is only available after Ethereum's cancun hard fork and on Solidity versions >= 0.8.24 - some config must be set in `foundry.toml` config file. + +To do that, add the following lines to `foundry.toml`: + +```toml +# foundry.toml + +solc_version = "0.8.26" +evm_version = "cancun" +ffi = true +``` + +Awesome! Now it's all set to start building the hook! Let’s run a quick test to confirm everything is set up properly. + +## Compile a Basic Hook Contract + +To confirm that the environment is configured correctly let's write a basic Counter Hook contract. Create a new file, `./src/CounterHook.sol` and paste the following code into it: + +```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 CounterHook is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeSwapCount; + mapping(PoolId => uint256 count) public afterSwapCount; + + mapping(PoolId => uint256 count) public beforeAddLiquidityCount; + mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + // ----------------------------------------------- + // NOTE: see IHooks.sol for function documentation + // ----------------------------------------------- + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + beforeSwapCount[key.toId()]++; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4, int128) + { + afterSwapCount[key.toId()]++; + return (BaseHook.afterSwap.selector, 0); + } + + function beforeAddLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + beforeAddLiquidityCount[key.toId()]++; + return BaseHook.beforeAddLiquidity.selector; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + beforeRemoveLiquidityCount[key.toId()]++; + return BaseHook.beforeRemoveLiquidity.selector; + } +} +``` + +To compile the Counter Hook contracts in the `./src` folder, use the foundry build command: + +```bash +forge build +``` + +If the environment is compiled correctly it will display a message: + +``` +Compiler run successful! +``` + +## Local Node via Anvil + +Other than writing unit tests, [Anvil](https://book.getfoundry.sh/anvil/) can be used to deploy and test hooks. + +With the local node up and running, use the `--rpc-url 127.0.0.1:8545` flag in tests to point the Foundry testing suite to that local node: + +```bash +# start anvil, a local EVM chain +anvil + +# in a new terminal +# foundry script for deploying v4 & hooks to anvil +forge script script/Anvil.s.sol \ + --rpc-url http://localhost:8545 \ + --private-key \ + --broadcast + +# test on the anvil local node +forge test --rpc-url 127.0.0.1:8545 +``` + +# Next Steps +With the environment set up ready to be built on. Jump over to the guides section to learn about the Uniswap functions that can be integrated with Hook. Remember to add all contracts (.sol files) to the `./src` folder and their subsequent tests to the `./test` folder. Then test them against the local anvil node by running: + +```bash +forge test --rpc-url 127.0.0.1:8545 +``` + diff --git a/docs/contracts/v4/guides/04-hooks/01-swap.mdx b/docs/contracts/v4/guides/04-hooks/01-swap.mdx new file mode 100644 index 0000000000..fb2bfc41a7 --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/01-swap.mdx @@ -0,0 +1,235 @@ +--- +title: Swap Hooks +--- + +Swaps are the most common interaction with the Uniswap protocol. When it comes to swap there are two hook functions available to customize and extend its behavior: + +- `beforeSwap` +- `afterSwap` + +As the names suggest `beforeSwap`/`afterSwap` are functions called before or after a swap is executed on a pool. + +This guide will explain the mechanism of encoded flags for hooks, and go through the parameters and examples for `beforeSwap` and `afterSwap`. + +Note: The swap examples are not production ready code, and are implemented in a simplistic manner for the purpose of learning. + +## Hook Flags + +As mentioned in [Concept of Hooks](../../concepts/04-hooks.mdx), hook contracts indicate their implemented functions by __encoding flags in the address of the contract__. The `PoolManager` uses these permissions to determine which hook functions to call for a given pool. + +Each hook function e.g. `beforeSwap` - corresponds to a certain _flag_. For example, the `beforeSwap` function is correlated to the [`BEFORE_SWAP_FLAG`](https://github.com/Uniswap/v4-core/blob/main/src/libraries/Hooks.sol#L37) which has a value of `1 << 7`. + +These flags represent specific bits in the address of the hook smart contract - and the value of the bit (a one or a zero) represents whether that flag is true or false. An example: + +Addresses on Ethereum are 20 bytes long (160 bits). So for example the address: + +``` +0x00000000000000000000000000000000000000C0 +``` + +represented in binary is: + +```solidity +0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 +0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 +0000 0000 0000 0000 0000 0000 1100 0000 +``` + +In binary it goes from right-to-left - so the trailing 8 bits of this address are `1100 0000` where: + +1st Bit to 6th Bit = `0` + +7th Bit to 8th Bit = `1` + +The `AFTER_SWAP` flag is represented by the 7th bit - which is set to `1` for the example contract address. In the `PoolManager's` swap execution flow, it will observe the flag and make a call to the hook's `afterSwap` function. + +Similarly, the 8th bit which is also a `1`, actually corresponds to the `BEFORE_SWAP` i.e. the `beforeSwap` hook function - which will also be called by the `PoolManager` during a `swap` workflow. + +A full list of all flags can be found [here](https://github.com/Uniswap/v4-core/blob/main/src/libraries/Hooks.sol). + +## Set Up the Contract + +Declare the solidity version used to compile the contract, since transient storage is used the solidity version will be `>=0.8.24`. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +``` + +Import the relevant dependencies from `v4-core` and `v4-periphery`: + +```solidity +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"; +``` + +Create a contract called SwapHook, use `PoolIdLibrary` to attach functions of computing ID of a pool to `PoolKey`. Declare two mappings to act as counters when calling `beforeSwap` and `afterSwap`. + +```solidity +contract SwapHook is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeSwapCount; + mapping(PoolId => uint256 count) public afterSwapCount; + + constructor(IPoolManager _poolManager) BaseHook(_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. + +```solidity +function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); +} +``` + +## beforeSwap + +Here the example shows that every time __before__ a swap is executed in a pool, `beforeSwapCount` for that pool will be incremented by one. + +```solidity +function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) +{ + beforeSwapCount[key.toId()]++; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); +} +``` + +### `beforeSwap` Parameters + +When triggering the `beforeSwap` hook function, there are some parameters we can make use of to customize or extend the behavior of `swap`. These parameters are described in `beforeSwap` from [`IHooks.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol#L111). + +A brief overview of the parameters: +- `sender` The initial `msg.sender` for the `PoolManager.swap` call. Typically a swap router +- `key` The key for the pool +- `params` The parameters for the swap i.e. `SwapParams` from [`IPoolManager.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol#L146C12-L146C22) +- `hookData` Arbitrary data handed into the `PoolManager` by the swapper to be be passed on to the hook + +## afterSwap + +Similiar as above, every time __after__ a swap is executed in a pool, `afterSwapCount` for that pool will be incremented by one. + +```solidity +function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4, int128) +{ + afterSwapCount[key.toId()]++; + return (BaseHook.afterSwap.selector, 0); +} +``` + +### `afterSwap` Parameters + +When triggering the `afterSwap` hook function, there are some parameters we can make use of to customize or extend the behavior of `swap`. These parameters are described in `afterSwap` from [`IHooks.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol#L126). + +A brief overview of the parameters: +- `sender` The initial `msg.sender` for the `PoolManager.swap` call. Typically a swap router +- `key` The key for the pool +- `params` The parameters for the swap i.e. `SwapParams` from [`IPoolManager.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol#L146C12-L146C22) +- `delta` The amount owed to the caller (positive) or owed to the pool (negative) +- `hookData` Arbitrary data handed into the `PoolManager` by the swapper to be be passed on to the hook + + + +## A Complete Swap Hook Contract + +```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 SwapHook is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeSwapCount; + mapping(PoolId => uint256 count) public afterSwapCount; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + // ----------------------------------------------- + // NOTE: see IHooks.sol for function documentation + // ----------------------------------------------- + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + beforeSwapCount[key.toId()]++; + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function afterSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata, BalanceDelta, bytes calldata) + external + override + returns (bytes4, int128) + { + afterSwapCount[key.toId()]++; + return (BaseHook.afterSwap.selector, 0); + } +} +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/04-hooks/02-liquidity.mdx b/docs/contracts/v4/guides/04-hooks/02-liquidity.mdx new file mode 100644 index 0000000000..00a527343f --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/02-liquidity.mdx @@ -0,0 +1,205 @@ +--- +title: Liquidity Hooks +--- + +This guide will walk through on an example of adding and removing liquidity. There are four hook functions available to customize and extend these behavior: + +- `beforeAddLiquidity` +- `afterAddLiquidity` +- `beforeRemoveLiquidity` +- `afterRemoveLiquidity` + +As the names suggest `beforeAddLiquidity`/`afterAddLiquidity` are functions called before or after liquidity is added to a pool. +Similarly `beforeRemoveLiquidity`/`afterRemoveLiquidity` are functions called before or after liquidity is removed from a pool. + +This guide will go through the parameters and examples specifically for `beforeAddLiquidity` and `beforeRemoveLiquidity`. + +Note: The liquidity examples are not production ready code, and are implemented in a simplistic manner for the purpose of learning. + +## Set Up the Contract + +Declare the solidity version used to compile the contract, since transient storage is used the solidity version will be `>=0.8.24`. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +``` + +Import the relevant dependencies from `v4-core` and `v4-periphery`: + +```solidity +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"; +``` + +Create a contract called LiquidityHook, use `PoolIdLibrary` to attach functions of computing ID of a pool to `PoolKey`. Declare two mappings to act as counters when calling `beforeAddLiquidity` and `beforeRemoveLiquidity`. + +```solidity +contract LiquidityHook is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeAddLiquidityCount; + mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount; + + constructor(IPoolManager _poolManager) BaseHook(_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. + +```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: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeAddLiquidityReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); +} +``` + +## beforeAddLiquidity + +Here the example shows that every time __before__ liquidity is added to a pool, `beforeAddLiquidityCount` for that pool will be incremented by one. + +```solidity +function beforeAddLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata +) external override returns (bytes4) { + beforeAddLiquidityCount[key.toId()]++; + return BaseHook.beforeAddLiquidity.selector; +} +``` + +### `beforeAddLiquidity` Parameters + +When triggering the `beforeAddLiquidity` hook function, there are some parameters we can make use of to customize or extend the behavior of `modifyLiquidity`. These parameters are described in `beforeAddLiquidity` from [`IHooks.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol#L47). + +A brief overview of the parameters: +- `sender` The initial `msg.sender` for the add liquidity call +- `key` The key for the pool +- `params` The parameters for adding liquidity i.e. `ModifyLiquidityParams` from [`IPoolManager.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol#L125C12-L125C33) +- `hookData` Arbitrary data handed into the `PoolManager` by the liquidity provider to be be passed on to the hook + +## beforeRemoveLiquidity + +Similiar as above, every time __before__ liquidity is removed from a pool, `beforeRemoveLiquidityCount` for that pool will be incremented by one. + +```solidity +function beforeRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata +) external override returns (bytes4) { + beforeRemoveLiquidityCount[key.toId()]++; + return BaseHook.beforeRemoveLiquidity.selector; +} +``` + +### `beforeRemoveLiquidity` Parameters + +When triggering the `beforeRemoveLiquidity` hook function, there are some parameters we can make use of to customize or extend the behavior of `modifyLiquidity`. These parameters are described in `beforeRemoveLiquidity` from [`IHooks.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IHooks.sol#L78). + +A brief overview of the parameters: +- `sender` The initial msg.sender for the remove liquidity call +- `key` The key for the pool +- `params` The parameters for removing liquidity i.e. `ModifyLiquidityParams` from [`IPoolManager.sol`](https://github.com/Uniswap/v4-core/blob/main/src/interfaces/IPoolManager.sol#L125C12-L125C33) +- `hookData` Arbitrary data handed into the `PoolManager` by the liquidity provider to be be passed on to the hook + +## A Complete Liquidity Hook Contract + +```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 LiquidityHook is BaseHook { + using PoolIdLibrary for PoolKey; + + // NOTE: --------------------------------------------------------- + // state variables should typically be unique to a pool + // a single hook contract should be able to service multiple pools + // --------------------------------------------------------------- + + mapping(PoolId => uint256 count) public beforeAddLiquidityCount; + mapping(PoolId => uint256 count) public beforeRemoveLiquidityCount; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: false, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeAddLiquidityReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + // ----------------------------------------------- + // NOTE: see IHooks.sol for function documentation + // ----------------------------------------------- + + 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; + } +} +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/04-hooks/03-NoOp.mdx b/docs/contracts/v4/guides/04-hooks/03-NoOp.mdx new file mode 100644 index 0000000000..d47d38a3a8 --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/03-NoOp.mdx @@ -0,0 +1,131 @@ +--- +title: NoOp Hooks +--- + +One feature enabled by custom accounting is​​​​‌ NoOp swap. This feature allows hook developers to replace the v4 (v3-style) swap logic. + +This means developers can replace Uniswap's internal core logic for how to handle swaps. Two emergent use-cases are possible with NoOp: + +1. Asynchronous swaps and swap-ordering. Delay the v4 swap logic for fulfillment at a later time. +2. Custom Curves. Replace the v4 swap logic with different swap logic. The custom logic is flexible and developers can implement symmetric curves, asymmetric curves, or custom quoting. + +> NoOp is typically described as taking the full input to replace the internal swap logic, partially taking the input is better described as *custom accounting* + +Note: The flexibility of NoOp means hook developers can implement harmful behavior (such as taking all swap amounts for themselves, charging extra fees, etc.). Hooks with NoOp behavior should be examined very closely by both developers and users. + +# Configure a NoOp Hook + +To enable NoOp, developers will need the hook permission `BEFORE_SWAP_RETURNS_DELTA_FLAG` + +```solidity +import {BaseHook} from "v4-periphery/BaseHook.sol"; +// ... + +contract NoOpHook is BaseHook { + // ... + + function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + afterAddLiquidity: false, + beforeRemoveLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: false, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: true, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + // ... +} +``` + +# beforeSwap + +NoOp only works on exact-input swaps and the *beforeSwap* **must** take the input currency and return `BeforeSwapDelta`. The hook should `IPoolManager.mint` itself the corresponding tokens equal to the amount of the input (`amountSpecified`). It should then return a `BeforeSwapDelta` where `deltaSpecified = -amountSpecified` (the positive amount). + +The funds' movements are as follows: + +1. User initiates a swap, specifying -100 tokenA as input +2. The hook's beforeSwap takes 100 tokenA for itself, and returns a value of 100 to PoolManager. +3. The PoolManager accounts the 100 tokens against the swap input, leaving 0 tokens remaining +4. The PoolManager does not execute swap logic, as there are no tokens left to swap +5. The PoolManager transfers the delta from the hook to the swap router, in step 2 the hook created a debt (that must be paid) +6. The swap router pays off the debt using the user's tokens + +```solidity +contract NoOpHook is BaseHook { + // ... + + function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + // NoOp only works on exact-input swaps + if (params.amountSpecified < 0) { + // take the input token so that v3-swap is skipped... + Currency input = params.zeroForOne ? key.currency0 : key.currency1; + uint256 amountTaken = uint256(-params.amountSpecified); + poolManager.mint(address(this), input.toId(), amountTaken); + + // to NoOp the exact input, we return the amount that's taken by the hook + return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(amountTaken.toInt128(), 0), 0); + } + else { + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO, 0); + } + + } +} +``` + +# Testing + +To verify the NoOp behaved properly, developers should test the swap and that token balances match expected behavior. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {Deployers} from "v4-core/test/utils/Deployers.sol"; +// ... + +contract NoOpSwapTest is Test, Deployers { + // ... + + function setUp() public { + // ... + } + + function test_noOp() public { + assertEq(hook.beforeSwapCount(poolId), 0); + + uint256 balance0Before = currency0.balanceOfSelf(); + uint256 balance1Before = currency1.balanceOfSelf(); + + // Perform a test swap // + int256 amount = -1e18; + bool zeroForOne = true; + BalanceDelta swapDelta = swap(poolKey, zeroForOne, amount, ZERO_BYTES); + // ------------------- // + + uint256 balance0After = currency0.balanceOfSelf(); + uint256 balance1After = currency1.balanceOfSelf(); + + // user paid token0 + assertEq(balance0Before - balance0After, 1e18); + + // user did not recieve token1 (NoOp) + assertEq(balance1Before, balance1After); + } +} +``` \ No newline at end of file diff --git a/docs/contracts/v4/guides/04-hooks/04-Volatility-fee-hook.mdx b/docs/contracts/v4/guides/04-hooks/04-Volatility-fee-hook.mdx new file mode 100644 index 0000000000..d48c7abfa5 --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/04-Volatility-fee-hook.mdx @@ -0,0 +1,186 @@ +--- +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/04-hooks/_category_.json b/docs/contracts/v4/guides/04-hooks/_category_.json new file mode 100644 index 0000000000..52417c9705 --- /dev/null +++ b/docs/contracts/v4/guides/04-hooks/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Hooks", + "collapsed": true + } + \ No newline at end of file diff --git a/docs/contracts/v4/guides/05-subscriber.mdx b/docs/contracts/v4/guides/05-subscriber.mdx new file mode 100644 index 0000000000..3d407797e6 --- /dev/null +++ b/docs/contracts/v4/guides/05-subscriber.mdx @@ -0,0 +1,47 @@ +--- +title: Subscriber +--- + +# Context + +For developers looking to support custom _liquidity mining_, subscriber contracts receive notifications about a position. +Rewards can be issued proportional to the liquidity's fee revenue, without the need of custodying the position. + +--- + +# Guide + +## 1. Inherit `ISubscriber` + +Please see [`ISubscriber`](https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/ISubscriber.sol) for the interface definition + +```solidity +import {ISubscriber} from "v4-periphery/src/interfaces/ISubscriber.sol"; + +contract MySubscriber is ISubscriber { + // Implement the ISubscriber interface +} +``` + +Developers should implement each function, and do proper accounting for rewards -- rewards should be proportional +to a position's liquidity or fee-revenue. + +## 2. A note on `unsubscribe()` + +Unsubscribe has a gas limit to prevent griefing. The value is determined at deployment, and is readable via `unsubscribeGasLimit()` on `PositionManager` + +If `notifyUnsubscribe()` reverts, the position will still successfully unsubscribe. + +## 3. Opt-in to a subscriber contract + +To opt-in to a subscriber contract, call `subscribe()` on the `PositionManager` contract. + +```solidity +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; + +IPositionManager posm = IPositionManager(
); +ISubscriber mySubscriber = ISubscriber(
); + +bytes memory optionalData = ...; +posm.subscribe(tokenId, mySubscriber, optionalData); +``` diff --git a/docs/contracts/v4/guides/06-unlock-callback.mdx b/docs/contracts/v4/guides/06-unlock-callback.mdx new file mode 100644 index 0000000000..866c7a2cf1 --- /dev/null +++ b/docs/contracts/v4/guides/06-unlock-callback.mdx @@ -0,0 +1,99 @@ +--- +title: Unlock Callback & Deltas +--- + +### Refresher + +In order to have access to the liquidity inside the `PoolManager`, +it needs to be _unlocked_ to begin with. After being unlocked, any +number of operations can be executed, which at the end of must be _locked_ +again. At this point, if there are any _non-zero deltas_, meaning the +PoolManager is owed or owes tokens back to some address, the whole +execution reverts. Otherwise, both parties have paid or received +the right amount of tokens and the operations have successfully +carried out. + +# Unlocking the PoolManager + +### Implementing the unlock callback + +Prior to unlocking the PoolManager, the integrating contract must +implement the `unlockCallback` function. This function will be +called by the PoolManager after being unlocked. An easy way to +do this is to inherit the `SafeCallback` abstract contract. + +```solidity +import {SafeCallback} from "v4-periphery/src/base/SafeCallback.sol"; + +contract IntegratingContract is SafeCallback { + constructor(IPoolManager _poolManager) SafeCallback(_poolManager) {} +} +``` + +### Calling the unlock function + +After implementing the callback, the integrating contract can now +invoke the `unlock()` function. It receives a _bytes_ parameter +that is further passed to your callback function as an argument. +This parameter is used to encode the sequence of operations to be +executed in the context of the `PoolManager`. + + +```solidity +bytes memory unlockData = abi.encode(encode_operations_here); +bytes memory unlockResultData = poolManager.unlock(unlockData); +``` + +Next, we must override the `_unlockCallback` function inherited from +the `SafeCallback` contract. In your implementation, you should +decode your operations and continue with the desired logic. + +```solidity +function _unlockCallback(bytes calldata data) internal override returns (bytes memory) { + (...) = abi.decode(data, (...)); +} +``` + +# Operations + +There are **9** operations that can be done in the `PoolManager` +which fall in two categories: _liquidity-accessing_ and _delta-resolving_. + +### Deltas + +Deltas are the `PoolManager`'s method to keep track of token amounts it +needs to receive, respectively to distribute. A negative delta signals that +the `PoolManager` is owed tokens, while a positive one expresses a +token balance that needs to be paid to its user. + +### Liquidity-accessing + +_Liquidity-accessing_ operations will create non-zero _deltas_ and +produce a state transition of the selected pool. +They are the following: + +* _modify liquidity_ - used to increase or decrease liquidity; increasing +liquidity will result in a negative token delta, while decreasing yields a positive one +* _swap_ - used to trade one token for another; will result in a negative tokenA delta +and a positive tokenB delta +* _donate_ - used to provide direct token revenue to positions in range; +will result in a negative delta for the pool's tokens the user wishes +to provide + +### Delta-resolving + +_Delta-resolving_ operations are used to even out the deltas created +by the _liquidity-accessing_ operations. +They are the following: + +* _settle_ - used following token transfers to the manager +or burning of ERC6909 claims to resolve negative deltas +* _take_ - transfer tokens from the manager, used to resolve +positive deltas but also provide token loans, producing negative deltas +* _mint_ - used to create ERC6909 claims, creating a negative delta +that needs to be resolved by transferring the corresponding token and +_settling_ afterwards +* _burn_ - removes ERC6909 claims, creating a positive delta for tokens to +be transferred back to the owner or used in settling negative balances +* _clear_ - used to zero out positive token deltas, helpful to forfeit +insignificant token amounts in order to avoid paying further transfer costs \ No newline at end of file diff --git a/docs/contracts/v4/guides/07-read-pool-state.mdx b/docs/contracts/v4/guides/07-read-pool-state.mdx new file mode 100644 index 0000000000..9c29a4cd15 --- /dev/null +++ b/docs/contracts/v4/guides/07-read-pool-state.mdx @@ -0,0 +1,9 @@ +--- +title: Reading Pool State +--- + +## 🚧 Under construction 🚧 + +Please see [StateLibrary](https://github.com/Uniswap/v4-core/blob/main/src/libraries/StateLibrary.sol) + +and how it's used in [tests](https://github.com/Uniswap/v4-core/blob/main/test/PoolManagerInitialize.t.sol#L91) \ No newline at end of file diff --git a/docs/contracts/v4/guides/_category_.json b/docs/contracts/v4/guides/_category_.json new file mode 100644 index 0000000000..30dc62ac64 --- /dev/null +++ b/docs/contracts/v4/guides/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Guides", + "position": 4, + "collapsed": false +} diff --git a/docs/contracts/v4/overview.mdx b/docs/contracts/v4/overview.mdx index cef18ea325..d31d176363 100644 --- a/docs/contracts/v4/overview.mdx +++ b/docs/contracts/v4/overview.mdx @@ -4,25 +4,58 @@ title: Overview sidebar_position: 1 --- -# Uniswap V4 - Hooks +# Uniswap v4 -Welcome to the Uniswap V4 developer documentation. +Uniswap v4 inherits all of the capital efficiency gains of Uniswap v3, but provides flexibility via *hooks* and gas optimizations across the entire lifecycle. -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. +For additional information, see the [Uniswap v4 whitepaper](https://github.com/Uniswap/v4-core/blob/main/docs/whitepaper/whitepaper-v4.pdf) -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. +--- + + +## Hooks + +Developers can attach solidity logic to the _swap lifecycle_ through Hooks. The logic is executed before and/or after major operations such as +pool creation, liquidity addition and removal, swapping, and donations. Hooks are deployed contracts, and are called by the Uniswap v4 PoolManager, +for permissionless execution. + +The flexibility of hooks can enable: +* Limit orders +* Custom oracles +* Fee management +* Automated liquidity management + +## Dynamic Fees + +Uniswap v4 supports dynamic fees, allowing pools to adjust their fees up or down. While other AMMs may have hard-coded logic for dynamic fees, +v4 provides no opinionated calculation of the fee. The frequency of *liquidity fee* updates is also flexible and determined by the developer. Fee updates can +occur on every swap, every block, or on an arbitrary schedule (weekly, monthly, yearly, etc). + +Dynamic fees open up the design space for fee optimization, value redistribution, and research. + + +## Singleton Design + +Architecturally, all pool state and operations are managed by a single contract -- `PoolManager.sol`. The singleton design provides major gas +savings. For example, creating a pool is now a state update instead of the deployment of a new contract. Swapping through multiple pools no longer requires +transferring tokens for intermediate pools. + +## Flash Accounting + +By leveraging EIP-1153 Transient Storage, v4 provides an optimization referred to as *flash accounting*. Swapping, liquidity modification, and donations +incur *balance changes*, i.e. tokens to be sent in and tokens to be taken out. With *flash accounting* these balance changes are efficiently recorded in transient storage and +netted against each other. This system allows users to only pay the final balance change, without the need for resolving intermediate balance changes. + +## Native ETH + +Uniswap v4 supports native token assets (Ether), without the need to wrap/unwrap the native token to Wrapped Ether (WETH9). -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. +## Custom Accounting -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. +The flexibility of custom accounting allows developers to alter token amounts for swaps and liquidity modifications. The feature opens up the design +space for hooks to charge fees or forgo the underlying concentrated liquidity model. -Note: V4 is currently in development, and changes are ongoing in the contracts. Updates to the documentation -will be provided accordingly. +Example use-cases: +* Custom curves, opt-out of the concentrated liquidity curve in favor of an entirely independent pricing mechanism +* Hook swap fees, charge and collect fees on swaps +* Liquidity withdrawal fees, penalize and/or redistribute fee revenue diff --git a/docs/contracts/v4/reference/_category_.json b/docs/contracts/v4/reference/_category_.json new file mode 100644 index 0000000000..3661fbbedb --- /dev/null +++ b/docs/contracts/v4/reference/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Technical Reference", + "position": 5, + "collapsed": true +} diff --git a/docs/contracts/v4/reference/core/IPoolManager.mdx b/docs/contracts/v4/reference/core/IPoolManager.mdx new file mode 100644 index 0000000000..a3c0bae854 --- /dev/null +++ b/docs/contracts/v4/reference/core/IPoolManager.mdx @@ -0,0 +1,118 @@ +--- +title: IPoolManager +--- + +The `IPoolManager` interface defines the main methods for interacting with the Uniswap V4 pool manager contract. It exposes the core _swap lifecycle_ operations + +# ModifyLiquidityParams + +Structure used to modify liquidity in a pool. + +- `tickLower`: Lower tick boundary of the position +- `tickUpper`: Upper tick boundary of the position +- `liquidityDelta`: Amount of liquidity to add (positive) or remove (negative) +- `salt`: A value to set if you want unique liquidity positions at the same range + +Used in the `modifyLiquidity` function to add or remove liquidity from a specific position in the pool. + +# SwapParams + +Structure used to execute a swap in a pool. + +- `zeroForOne`: Direction of the swap (true for token0 to token1, false for token1 to token0) +- `amountSpecified`: Amount of tokens to swap (positive for exact input, negative for exact output) +- `sqrtPriceLimitX96`: Slippage limit represented as [Q64X96](https://uniswapv3book.com/milestone_3/more-on-fixed-point-numbers.html#:~:text=The%20Q64.,and%2018%20signify%20decimal%20places.) notation + +Used in the `swap` function to define the behavior of our swap. + +# Methods + +## initialize + +```solidity +function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData) + external + returns (int24 tick); +``` + +Initialize a new pool by defining its parameters: token pair, fee tier, tick spacing, hook contract, and starting price + +| Param Name | Type | Description | +|---------------|-----------|--------------------------------------------------| +| key | PoolKey | The key defining the pool to initialize | +| sqrtPriceX96 | uint160 | The initial sqrt price of the pool as a Q64.96 value | +| hookData | bytes | Any additional data to pass to our hook contract on the before/after initialization hooks | + +Returns the initial tick value of the pool. + +## unlock + +```solidity +function unlock(bytes calldata data) external returns (bytes memory); +``` + +Provides a single entry point for all pool operations. The provided data is passed to the callback for execution. + +| Param Name | Type | Description | +|------------|-------|--------------------------------------------------------------------------------------| +| data | bytes | Any data to pass to the callback via `IUnlockCallback(msg.sender).unlockCallback(data)` | + +Returns the data returned by the callback. + +## modifyLiquidity + +```solidity +function modifyLiquidity( + PoolKey memory key, + ModifyLiquidityParams memory params, + bytes calldata hookData +) external returns (BalanceDelta, BalanceDelta); +``` + +Modifies the liquidity for the given pool. Can be used to add or remove liquidity, or collect fees +> passing zero will collect fees for the given tick range + +| Param Name | Type | Description | +|------------|------------------------|--------------------------------------------------| +| key | PoolKey | The key of the pool to modify liquidity in | +| params | ModifyLiquidityParams | The parameters for modifying the liquidity position | +| hookData | bytes | Any data to pass to a hook contract on the before/add liquidity hooks | + +Returns the balance delta for the caller (total of principal and fees) and the fee delta generated in the liquidity range. + +## swap + +```solidity +function swap(PoolKey memory key, SwapParams memory params, bytes calldata hookData) + external + returns (BalanceDelta); +``` + +Executes a swap against the given pool using the provided parameters. + +| Param Name | Type | Description | +|------------|------------|-----------------------------------------| +| key | PoolKey | The key of the pool to swap in | +| params | SwapParams | The parameters for executing the swap | +| hookData | bytes | Any data to pass to a hook contract on the before/afterSwap hooks | + +Returns the balance delta for the address initiating the swap. Note swapping on low liquidity pools may cause unexpected amounts. Interacting with certain hooks may also alter the swap amounts. + +## donate + +```solidity +function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) + external + returns (BalanceDelta); +``` + +Donates the specified currency amounts to the pool. + +| Param Name | Type | Description | +|------------|----------|-------------------------------------| +| key | PoolKey | The key of the pool to donate to | +| amount0 | uint256 | The amount of token0 to donate | +| amount1 | uint256 | The amount of token1 to donate | +| hookData | bytes | Any data to pass to a hook contract on the before/afterDonate hooks| + +Returns the balance delta representing the donated amounts. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/_category_.json b/docs/contracts/v4/reference/core/_category_.json new file mode 100644 index 0000000000..255eb73d59 --- /dev/null +++ b/docs/contracts/v4/reference/core/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Core", + "position": 1, + "collapsed": false +} diff --git a/docs/contracts/v4/reference/core/libraries/_category_.json b/docs/contracts/v4/reference/core/libraries/_category_.json new file mode 100644 index 0000000000..57982ec8d2 --- /dev/null +++ b/docs/contracts/v4/reference/core/libraries/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Libraries", + "position": 0, + "collapsed": true +} diff --git a/docs/contracts/v4/reference/core/libraries/liquidity-amounts.mdx b/docs/contracts/v4/reference/core/libraries/liquidity-amounts.mdx new file mode 100644 index 0000000000..641741dbd1 --- /dev/null +++ b/docs/contracts/v4/reference/core/libraries/liquidity-amounts.mdx @@ -0,0 +1,133 @@ +--- +title: LiquidityAmounts +--- + +The `LiquidityAmounts` library provides functions for computing liquidity amounts from token amounts and prices in Uniswap V4. + +# Key Concept: sqrtPriceX96 + +`sqrtPriceX96` represents the square root of the price ratio of token1 to token0, multiplied by 2^96. This representation allows for precise price calculations across a wide range of values while using fixed-point arithmetic. It's more efficient than using ticks for intermediate calculations, as it avoids frequent conversions between prices and ticks. + +# Functions + +## getLiquidityForAmount0 + +```solidity +function getLiquidityForAmount0(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount0) +internal +pure +returns (uint128 liquidity) +``` + +Computes the amount of liquidity received for a given amount of token0 and price range. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| amount0 | uint256 | The amount of token0 being sent in | + +Returns the amount of liquidity received. + +## getLiquidityForAmount1 + +```solidity +function getLiquidityForAmount1(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint256 amount1) +internal +pure +returns (uint128 liquidity) +``` + +Computes the amount of liquidity received for a given amount of token1 and price range. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| amount1 | uint256 | The amount of token1 being sent in | + +Returns the amount of liquidity received. + +## getLiquidityForAmounts + +```solidity +function getLiquidityForAmounts( +uint160 sqrtPriceX96, +uint160 sqrtPriceAX96, +uint160 sqrtPriceBX96, +uint256 amount0, +uint256 amount1 +) internal pure returns (uint128 liquidity) +``` + +Computes the maximum amount of liquidity received for given amounts of token0 and token1, the current pool prices, and the prices at the tick boundaries. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceX96 | uint160 | Current square root price of the pool | +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| amount0 | uint256 | The amount of token0 being sent in | +| amount1 | uint256 | The amount of token1 being sent in | + +Returns the maximum amount of liquidity received. + +## getAmount0ForLiquidity + +```solidity +function getAmount0ForLiquidity(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity) +internal +pure +returns (uint256 amount0) +``` + +Computes the amount of token0 for a given amount of liquidity and a price range. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| liquidity | uint128 | The liquidity being valued | + +Returns the amount of token0. + +## getAmount1ForLiquidity + +```solidity +function getAmount1ForLiquidity(uint160 sqrtPriceAX96, uint160 sqrtPriceBX96, uint128 liquidity) +internal +pure +returns (uint256 amount1) +``` + +Computes the amount of token1 for a given amount of liquidity and a price range. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| liquidity | uint128 | The liquidity being valued | + +Returns the amount of token1. + +## getAmountsForLiquidity + +```solidity +function getAmountsForLiquidity( +uint160 sqrtPriceX96, +uint160 sqrtPriceAX96, +uint160 sqrtPriceBX96, +uint128 liquidity +) internal pure returns (uint256 amount0, uint256 amount1) +``` + +Computes the token0 and token1 value for a given amount of liquidity, the current pool prices, and the prices at the tick boundaries. + +| Param Name | Type | Description | +|----------------|---------|-----------------------------------------------| +| sqrtPriceX96 | uint160 | Current square root price of the pool | +| sqrtPriceAX96 | uint160 | Square root of price at first tick boundary | +| sqrtPriceBX96 | uint160 | Square root of price at second tick boundary | +| liquidity | uint128 | The liquidity being valued | + +Returns the amount of token0 and token1. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/libraries/transient-state-library.mdx b/docs/contracts/v4/reference/core/libraries/transient-state-library.mdx new file mode 100644 index 0000000000..9df782a43f --- /dev/null +++ b/docs/contracts/v4/reference/core/libraries/transient-state-library.mdx @@ -0,0 +1,132 @@ +--- +title: TransientStateLibrary +--- + +The `TransientStateLibrary` is a crucial component of Uniswap V4, providing utility functions for managing transient state in the PoolManager contract. This library handles operations related to reserves, delta counts, and locking state, which are essential for the efficient and secure operation of the Uniswap V4 protocol. + +# Key Concepts + +## Transient Storage + +Uniswap V4 uses transient storage to optimize gas costs and improve efficiency. Transient storage, introduced in EIP-1153, is a way to store data that is only needed for the duration of a transaction, without persisting it to the blockchain's state trie. This is achieved using the `TLOAD` and `TSTORE` opcodes. + +Key points about transient storage in Uniswap V4: + +1. **PoolManager and** `exttload`: Instead of exposing custom getters for transient storage, the PoolManager implements an `exttload` (external tload) function. This function serves as an external wrapper for the `TLOAD` opcode, providing a standardized interface for accessing transient storage. +2. **TransientStateLibrary's Role:** The `TransientStateLibrary` acts as an intermediary, making calls to the PoolManager's `exttload` function to access transient storage. This abstraction simplifies the interaction with transient storage for other parts of the Uniswap V4 ecosystem. +3. **Standardization:** By channeling all transient storage access through the PoolManager's `exttload` function, Uniswap V4 ensures a consistent and controlled approach to managing transient data across the protocol. + +Common operations that involve transient state include: + +- Checking reserves (`getReserves`) +- Verifying currency deltas (`currencyDelta`) +- Syncing currency states (`sync`) +- Settling currency balances (`settle`) + +This architecture allows Uniswap V4 to benefit from the gas efficiency of transient storage while maintaining a clean and standardized interface for interacting with this temporary data. + +# Functions + +## getReserves + +```solidity +function getReserves(IPoolManager manager, Currency currency) internal view returns (uint256) +``` + +Retrieves the reserves of a specific currency from the PoolManager's transient storage. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------------------| +| manager | IPoolManager | The PoolManager contract instance | +| currency | Currency | The currency for which to fetch reserves | + +**Returns:** +- `uint256`: The amount of reserves for the specified currency + +**Notes:** +- Returns `0` if the reserves are not synced +- Returns `type(uint256).max` if the reserves are synced but the value is `0` + +## getNonzeroDeltaCount + +```solidity +function getNonzeroDeltaCount(IPoolManager manager) internal view returns (uint256) +``` + +Retrieves the count of nonzero deltas that must be zeroed out before the contract can be locked. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| manager | IPoolManager | The PoolManager contract instance | + +**Returns:** + +- `uint256`: The number of nonzero deltas + +## currencyDelta + +```solidity +function currencyDelta(IPoolManager manager, address caller_, Currency currency) internal view returns (int256) +``` + +Fetches the current delta for a specific caller and currency from the PoolManager's transient storage. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------------------| +| manager | IPoolManager | The PoolManager contract instance | +| caller_ | address | The address of the caller | +| currency | Currency | The currency for which to lookup the delta | + +**Returns:** + +- `int256`: The delta value for the specified caller and currency + +**Notes:** + +- A **negative** delta indicates an amount that must be **paid or settled** by the caller. In other words, a negative delta means the caller owes that amount and needs to pay or settle it. +- A **positive** delta indicates an amount that is owed to the caller. This delta amount must be **taken or claimed** by the caller. + +## isUnlocked + +```solidity +function isUnlocked(IPoolManager manager) internal view returns (bool) +``` + +Checks if the PoolManager contract is currently unlocked. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| manager | IPoolManager | The PoolManager contract instance | + +**Returns:** + +- `bool`: `true` if the contract is unlocked, `false` otherwise + +# Usage and Importance + +The `TransientStateLibrary` plays a critical role in Uniswap V4's operation: + +1. **Gas Optimization:** By using transient storage, the library helps reduce gas costs associated with state changes that are only relevant within a single transaction. This is particularly important for multi-hop transactions, where internal net balances (deltas) are updated instead of making token transfers for each hop. +2. **Security:** The library provides functions to check the lock state and manage deltas, which are crucial for maintaining the integrity of the protocol during operations. The use of transient storage also allows for more efficient implementation of security measures compared to V3's reentrancy guards. +3. **Flexibility:** The library allows for efficient management of currency-specific data, such as reserves and deltas, which is essential for Uniswap V4's multi-currency pools. +4. **Encapsulation:** By centralizing these utility functions in a library, the code promotes better organization and reusability across the Uniswap V4 codebase. + +# Integration with PoolManager + +The `TransientStateLibrary` is designed to work closely with the `PoolManager` contract. The `TransientStateLibrary` can be easily integrated with the `PoolManager` contract using the `using` keyword for syntactic sugar. This allows you to call the library functions as if they were methods of the `IPoolManager` instance. Here's an example: + +```solidity +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {TransientStateLibrary} from "v4-core/src/libraries/TransientStateLibrary.sol"; + +contract Example { + using TransientStateLibrary for IPoolManager; + + function example() external { + int256 delta = manager.currencyDelta(address(this), currency); + // Use the delta value... + } +} +``` + +In this example, the `using TransientStateLibrary for IPoolManager;` statement allows you to call `currencyDelta` directly on the `manager` instance, making your code more readable and concise. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/types/_category_.json b/docs/contracts/v4/reference/core/types/_category_.json new file mode 100644 index 0000000000..8ef8d93217 --- /dev/null +++ b/docs/contracts/v4/reference/core/types/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Types", + "position": 1, + "collapsed": true +} diff --git a/docs/contracts/v4/reference/core/types/balancedelta.mdx b/docs/contracts/v4/reference/core/types/balancedelta.mdx new file mode 100644 index 0000000000..aff5be7f69 --- /dev/null +++ b/docs/contracts/v4/reference/core/types/balancedelta.mdx @@ -0,0 +1,147 @@ +--- +title: BalanceDelta +--- + +`BalanceDelta` is a type used in Uniswap V4 to represent the balance changes of two tokens (token0 and token1). It tightly packs the two values in a single 256 bits. It is designed to efficiently store and manipulate these balance deltas, with the upper 128 bits representing the change in token0 (`amount0`) and the lower 128 bits representing the change in token1 (`amount1`). + +# Purpose + +The main purpose of `BalanceDelta` is to keep track of the net balance changes in the two tokens of a pool after various operations such as swaps, liquidity modifications, and interactions with hooks. It provides a compact and efficient way to store and update these balance deltas throughout the execution flow of the pool. + +In the context of hooks, `BalanceDelta` is used to ensure that the net balance change for each token is zero after the hook's functionality is executed. This is important for maintaining the integrity of the pool's balances and ensuring that the hooks do not introduce any unexpected or unauthorized balance changes. + +# Type Definition + +```solidity +type BalanceDelta is int256; +``` + +# Using Directives + +```solidity +using {add as +, sub as -, eq as ==, neq as !=} for BalanceDelta global; +using BalanceDeltaLibrary for BalanceDelta global; +using SafeCast for int256; +``` + +These using directives enable arithmetic operations, equality comparisons, and library functions to be used directly on `BalanceDelta` values. + +# Functions + +## toBalanceDelta + +```solidity +function toBalanceDelta(int128 amount0, int128 amount1) pure returns (BalanceDelta balanceDelta); +``` + +Creates a `BalanceDelta` value from two `int128` values representing `amount0` and `amount1`. + +| Param Name | Type | Description | +|------------|---------|--------------------------------------| +| amount0 | int128 | The amount for the first token | +| amount1 | int128 | The amount for the second token | + +Returns the created `BalanceDelta` value. + +## add + +```solidity +function add(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta); +``` + +Adds two `BalanceDelta` values. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| a | BalanceDelta | The first `BalanceDelta` value | +| b | BalanceDelta | The second `BalanceDelta` value | + +Returns the sum of the two `BalanceDelta` values. + +## sub + +```solidity +function sub(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta); +``` + +Subtracts one `BalanceDelta` value from another. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| a | BalanceDelta | The first `BalanceDelta` value | +| b | BalanceDelta | The second `BalanceDelta` value | + +Returns the difference of the two `BalanceDelta` values. + +## eq + +```solidity +function eq(BalanceDelta a, BalanceDelta b) pure returns (bool); +``` + +Checks if two `BalanceDelta` values are equal. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| a | BalanceDelta | The first `BalanceDelta` value | +| b | BalanceDelta | The second `BalanceDelta` value | + +Returns `true` if the values are equal, `false` otherwise. + +## neq + +```solidity +function neq(BalanceDelta a, BalanceDelta b) pure returns (bool); +``` + +Checks if two `BalanceDelta` values are not equal. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| a | BalanceDelta | The first `BalanceDelta` value | +| b | BalanceDelta | The second `BalanceDelta` value | + +Returns `true` if the values are not equal, `false` otherwise. + +# Library Functions + +## amount0 + +```solidity +function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0); +``` + +Extracts the `amount0` value from a `BalanceDelta`. + +| Param Name | Type | Description | +|--------------|--------------|--------------------------------------| +| balanceDelta | BalanceDelta | The `BalanceDelta` value | + +Returns the extracted `amount0` value as an `int128`. + +## amount1 + +```solidity +function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1); +``` + +Extracts the `amount1` value from a `BalanceDelta`. + +| Param Name | Type | Description | +|--------------|--------------|--------------------------------------| +| balanceDelta | BalanceDelta | The `BalanceDelta` value | + +Returns the extracted `amount1` value as an `int128`. + +# Usage in Hooks + +When a hook is called during a swap or liquidity modification, it can perform custom logic and interact with the pool's balances. However, to maintain the correctness of the pool's state, the hook must ensure that any balance changes it introduces are properly accounted for and net to zero at the end of its execution. +The BalanceDelta is forwarded to the `afterSwap` & `afterAddliquidity`, `afterRemoveLiquidity` hooks. + +# Usage in the Pool Library + +In the `Pool` library, `BalanceDelta` is used extensively to track balance changes during various operations such as swaps, liquidity modifications, and donations. The library functions `swap`, `modifyLiquidity`, and `donate` all return `BalanceDelta` values representing the net balance changes resulting from these operations. + +The `Pool` library uses `BalanceDelta` to efficiently update and manage the pool's balances, ensuring that the net balance changes are accurately accounted for and that the pool remains in a consistent state. + +By leveraging the compact representation and efficient arithmetic operations provided by `BalanceDelta`, the `Pool` library can perform complex balance calculations and updates in a gas-optimized manner, reducing the overall cost of executing pool-related operations. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/types/beforeswapdelta.mdx b/docs/contracts/v4/reference/core/types/beforeswapdelta.mdx new file mode 100644 index 0000000000..db7a6e8f7a --- /dev/null +++ b/docs/contracts/v4/reference/core/types/beforeswapdelta.mdx @@ -0,0 +1,429 @@ +--- +title: BeforeSwapDelta +--- + +`BeforeSwapDelta` is a custom type used in Uniswap V4 hook contracts to represent balance changes during swap operations. It is specifically designed to handle the return value of the `beforeSwap` hook and to be compatible with the `afterSwap` hook. + +Before explaining `BeforeSwapDelta` in detail, it is worth noting that in the context of Uniswap V4 swaps: + +- The **specified** token is the one for which the user specifies an exact input or output amount. +- The **unspecified** token is the counterpart in the swap, whose amount is determined by the pool's pricing mechanism. + +# Purpose + +The main purpose of `BeforeSwapDelta` is to efficiently encode and decode balance changes for both specified and unspecified tokens in a single 256-bit value. This compact representation allows for gas-efficient operations and seamless integration with Uniswap V4's hook system. + +`BeforeSwapDelta` is essential for: + +- Allowing hooks to modify swap parameters or override default swap behavior +- Allowing hooks to take fees from swaps +- Providing fine-grained control over balance adjustments resulting from swaps +- Optimizing gas usage by packing two `int128` values into a single `int256` + +To summarise, `BeforeSwapDelta` is used to ensure that the net balance change for each token is zero after the hook's functionality is executed. This is important for maintaining the integrity of the pool's balances and ensuring that the hooks do not introduce any unexpected or unauthorized balance changes. + +# Type Definition + +```solidity +type BeforeSwapDelta is int256; +``` + +The `BeforeSwapDelta` type is an alias for int256, where: + +- The upper 128 bits represent the delta in specified tokens +- The lower 128 bits represent the delta in unspecified tokens + +# Using Directives + +```solidity +using BeforeSwapDeltaLibrary for BeforeSwapDelta global; +using SafeCast for int256; +``` + +These using directives enable library functions to be used directly on `BeforeSwapDelta` values and provide safe casting operations for int256 values. + +# Functions + +## toBeforeSwapDelta + +```solidity +function toBeforeSwapDelta(int128 deltaSpecified, int128 deltaUnspecified) pure returns (BeforeSwapDelta beforeSwapDelta); +``` + +Creates a `BeforeSwapDelta` value from two `int128` values representing `deltaSpecified` and `deltaUnspecified`. + +| Param Name | Type | Description | +|------------|---------|--------------------------------------| +| deltaSpecified | int128 | The balance change for the specified token | +| deltaUnspecified | int128 | The balance change for the unspecified token | + +Returns the created `BeforeSwapDelta` value. + +This function uses bitwise operations in assembly for gas-efficient packing of the two int128 values: + +```solidity +assembly ("memory-safe") { + beforeSwapDelta := or(shl(128, deltaSpecified), and(sub(shl(128, 1), 1), deltaUnspecified)) +} +``` + +# Library Functions + +## ZERO_DELTA + +```solidity +BeforeSwapDelta public constant ZERO_DELTA = BeforeSwapDelta.wrap(0); +``` + +A constant representing a zero delta (no balance changes). It should be used as a default return value. It is most commonly used for hooks that are *not* implementing custom accounting. + +## getSpecifiedDelta + +```solidity +function getSpecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaSpecified); +``` + +Extracts the specified token delta from a `BeforeSwapDelta` value. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| delta | BeforeSwapDelta | The `BeforeSwapDelta` value | + +Returns the extracted specified token delta as an `int128`. + +## getUnspecifiedDelta + +```solidity +function getUnspecifiedDelta(BeforeSwapDelta delta) internal pure returns (int128 deltaUnspecified); +``` + +Extracts the unspecified token delta from a BeforeSwapDelta value. + +| Param Name | Type | Description | +|------------|--------------|--------------------------------------| +| delta | BeforeSwapDelta | The `BeforeSwapDelta` | + +Returns the extracted unspecified token delta as an `int128`. + +# Usage in Hooks + +When a hook is called during a swap operation, it can perform custom logic and interact with the pool's balances. The `beforeSwap` hook returns a `BeforeSwapDelta` value to indicate any balance changes the *hook* introduces. For example, hooks taking fees should return the value it took as a `BeforeSwapDelta`. + +# Usage in PoolManager.sol + +`BeforeSwapDelta` plays a crucial role in Uniswap V4's PoolManager contract, particularly in the swap process. Here's an overview of how it's used: + +## Calling the `beforeSwap` Hook + +In the `swap` function of the PoolManager contract, the `beforeSwap` hook is called: + +```solidity +function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) + // ... + returns (BalanceDelta swapDelta) +{ + // ... (other code) + + BeforeSwapDelta beforeSwapDelta; + { + int256 amountToSwap; + uint24 lpFeeOverride; + (amountToSwap, beforeSwapDelta, lpFeeOverride) = key.hooks.beforeSwap(key, params, hookData); + + // ... (swap execution) + } + + // ... (other code) +} +``` + +The `beforeSwap` hook returns a `BeforeSwapDelta` value along with other parameters. + +## Interaction between `beforeSwapDelta` and `amountToSwap` + +The `beforeSwapDelta` returned by the hook is used in conjunction with `params.amountSpecified` to determine the final `amountToSwap`. This allows hooks to modify the swap amount based on their custom logic. Here's a more detailed explanation of how this works: + +1. The `beforeSwap` hook returns a `BeforeSwapDelta` value. +2. The `getSpecifiedDelta()` of this `BeforeSwapDelta` is used to adjust the original `params.amountSpecified`. +3. This adjustment results in the final `amountToSwap` that will be used for the actual swap operation. + +Here's a simplified representation of this calculation: + +```solidity +int256 amountToSwap = params.amountSpecified + beforeSwapDelta.getSpecifiedDelta(); +``` + +In this example, the `amountToSwap` is calculated by adding the specified delta from `beforeSwapDelta` to the original `amountSpecified`. This calculation allows hooks to increase or decrease the swap amount, effectively implementing features like fees, rebates, or other custom logic. + +**Detailed Example:** + +Let's say a user wants to swap 100 tokens, but a hook implements a 1% fee: + +1. `params.amountSpecified` would be 100 +2. The hook calculates the fee as 1 token and returns a `beforeSwapDelta` with a specified delta of -1 +3. `amountToSwap` is then calculated as 100 + (-1) = 99 + +This way, the actual amount swapped (99) reflects the fee taken by the hook, while still allowing the pool to execute the swap based on the original 100 token input from the user. + +Here's how the `beforeSwap` hook might handle this: + +```solidity +function beforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata params, + bytes calldata +) external override returns (int256 amountIn, BeforeSwapDelta delta, uint24) { + int128 specifiedAmount = params.amountSpecified.toInt128(); + int128 fee = specifiedAmount / 100; // 1% fee + int128 adjustedAmount = specifiedAmount - fee; + + delta = BeforeSwapDelta.from(-fee, 0); // Fee taken from specified token + amountIn = params.amountSpecified; // Original amount + + return (amountIn, delta, 0); +} +``` + +After this hook executes: + +- `amountIn` remains 100 (the original `params.amountSpecified`) +- `delta` represents a change of -1 in the specified token (the fee) + +Then, in the PoolManager: + +```solidity +int256 amountToSwap = params.amountSpecified + beforeSwapDelta.getSpecifiedDelta(); +// This effectively calculates: 100 + (-1) = 99 +``` + +As a result: + +- The pool sees the full input amount of 100 tokens. +- The actual amount swapped is 99 tokens. +- The 1 token difference becomes the hook's fee. + +This mechanism allows hooks to influence the swap amount while maintaining transparency about the full input amount, enabling complex custom logic within the Uniswap V4 framework. + +## Relation to `afterSwap` + +While `beforeSwapDelta` is primarily used in the `beforeSwap` hook, it also plays a role in the `afterSwap` process. Specifically: + +- The `afterSwap` hook receives the `beforeSwapDelta` as a parameter. +- The unspecified delta (`beforeSwapDelta.getUnspecifiedDelta()`) is particularly important in the `afterSwap` context. +- This unspecified delta is accounted for in `afterSwap`'s calculations, allowing for consistent balance tracking across the entire swap process. + +This mechanism ensures that: + +1. Changes made by the `beforeSwap` hook are properly considered when finalizing the swap. +2. The `afterSwap` hook can make informed decisions based on the full context of the swap, including any modifications made in `beforeSwap`. +3. Complex swap logic can be implemented across multiple hook points while maintaining consistency. + +For example, if a fee was taken on the specified token in `beforeSwap`, the `afterSwap` hook can use this information to ensure the overall balance changes are correct, potentially adjusting the unspecified token amount accordingly. + +Developers implementing custom hooks should be aware of this relationship and ensure their `beforeSwap` and `afterSwap` implementations work together coherently, especially when implementing features like fees or rebates that affect token balances. + +# Key Purposes of BeforeSwapDelta + +The `BeforeSwapDelta` serves several important purposes in the Uniswap V4 swap process: + +1. **Customization of Swap Behavior:** It allows hooks to modify the swap parameters or even completely override the default swap behavior. +2. **Balance Adjustment:** The delta values can be used to adjust the final balance changes resulting from the swap, giving hooks fine-grained control over the swap's outcome. +3. **Gas Optimization:** By packing two `int128` values into a single `int256`, it reduces the number of stack variables and can lead to gas savings. +4. **Cross-Hook Communication:** It provides a way for the `beforeSwap` hook to pass information to the `afterSwap` hook, enabling more complex and stateful hook logic. +5. **Hook Fees Implementation:** `BeforeSwapDelta` offers flexible options for implementing hook fees: + +- Fees can be charged on either the specified or unspecified token. +- Fees can be implemented in either the `beforeSwap` or `afterSwap` hook. +- For `beforeSwap`: + +1. Adjust the specified amount to account for the fee. +2. Useful for scenarios where the fee needs to be known before the swap execution. + + +- For `afterSwap`: + +1. Generally considered best practice to charge fees on the unspecified token. +2. Allows for more accurate fee calculation based on the actual swap outcome. + +- As a result, developers can implement various fee structures, such as: + +1. Fixed fee amounts +2. Percentage-based fees +3. Tiered fee structures based on swap volume or other criteria + +Example of a simple percentage-based fee in `beforeSwap`: + +```solidity +int128 fee = specifiedAmount * FEE_PERCENTAGE / 100; +int128 adjustedAmount = specifiedAmount - fee; +delta = BeforeSwapDelta.from(-adjustedAmount, 0); +``` + +This flexibility in fee implementation allows developers to create sophisticated economic models within their Uniswap V4 hooks, tailoring the behavior to specific use cases while maintaining the efficiency and standardization provided by the `BeforeSwapDelta` structure. + +# Perspective + +It's important to note that the `BeforeSwapDelta` is from the perspective of the hook itself, not the user. For example, if a user swaps 1 USDC for 1 USDT: + +- User gives 1 USDC: balance0OfUser decreases +- Hook gets 1 USDC: balance0OfHook increases + +This perspective is key to correctly interpreting and manipulating the delta values within hook implementations. + +# Implementation Details + +The `BeforeSwapDelta` type and its associated functions use low-level assembly code for efficient bit manipulation and gas optimization: + +- The `toBeforeSwapDelta` function uses bitwise operations (`shl`, `or`, `and`, `sub`) to pack two `int128` values into a single `int256`. +- The `getSpecifiedDelta` function uses the `sar` (shift arithmetic right) operation to extract the upper 128 bits. +- The `getUnspecifiedDelta` function uses the `signextend` operation to extract and sign-extend the lower 128 bits. + +The `toBeforeSwapDelta` function combines the specified and unspecified deltas into a single `int256` value using bitwise operations. The `getSpecifiedDelta` and `getUnspecifiedDelta` functions extract the respective deltas using bit shifting and sign extension. + +By leveraging this compact representation and efficient arithmetic operations, Uniswap V4 can perform complex balance calculations and updates in a gas-optimized manner, reducing the overall cost of executing pool-related operations. + +# Implementation Considerations + +When working with `BeforeSwapDelta`, especially for implementing hook fees, consider the following: + +- **Fee Timing:** While fees can be implemented in either `beforeSwap` or `afterSwap`, charging fees on the unspecified token in `afterSwap` is often considered best practice. This approach can provide more accurate fee calculations based on the final swap amounts. +- **Fee Direction:** Remember that the deltas in `BeforeSwapDelta` are from the perspective of the hook. A positive delta means the hook is receiving tokens, while a negative delta means the hook is paying out tokens. +- **Consistency:** Ensure that your fee implementation is consistent across both `beforeSwap` and `afterSwap` hooks to maintain the integrity of the swap process. +- **Gas Efficiency:** When implementing fees, consider the gas costs of your calculations. The compact nature of `BeforeSwapDelta` can help in optimizing gas usage, but complex fee structures might increase gas costs. + +# Comparison with BalanceDelta + +`BeforeSwapDelta` shares a similar structure with `BalanceDelta`, both packing two `int128` values into a single `int256`. However, there are key differences: + +- `BalanceDelta` represents amount0 and amount1. +- `BeforeSwapDelta` represents specified and unspecified amounts, which may not directly correspond to token0 and token1, depending on the swap direction. + +# Best Practices + +When working with `BeforeSwapDelta`, consider the following best practices: + +- Always use the provided library functions (`getSpecifiedDelta` and `getUnspecifiedDelta`) to extract delta values. +- Ensure that the signs of the delta values are correct from the hook's perspective. +- Use `SafeCast` when converting between different integer types to prevent overflow/underflow errors. + +# Error Handling and Edge Cases + +- **Overflow/Underflow:** Ensure that the input int128 values do not exceed their range when packing into BeforeSwapDelta. +- **Zero Values:** ZERO_DELTA represents no balance changes. Be cautious when interpreting zero values in specific contexts. +- **Sign Mismatch:** Ensure that the signs of the delta values correctly represent the intended balance changes from the hook's perspective. + +# Example Usage in a Hook + +## Basic Example + +Here's a simple example of how `BeforeSwapDelta` might be used in a `beforeSwap` hook: + +```solidity +function beforeSwap( + address, + PoolKey calldata, + IPoolManager.SwapParams calldata params, + bytes calldata +) external override returns (int256 amountIn, BeforeSwapDelta delta, uint24) { + // Convert the specified amount to int128 + int128 specifiedAmount = params.amountSpecified.toInt128(); + + // In this example, we're not modifying the unspecified amount + int128 unspecifiedAmount = 0; // Calculated based on your custom logic + + // Create the BeforeSwapDelta + delta = toBeforeSwapDelta(specifiedAmount, unspecifiedAmount); + + // Return the original amount as amountIn + amountIn = params.amountSpecified; + + // Return 0 for lpFeeOverride as we're not changing the LP fee + return (amountIn, delta, 0); +} +``` + +Let's break down what this basic hook is doing: + +1. It converts the `params.amountSpecified` to `int128`, which is required for `BeforeSwapDelta`. +2. It sets the `unspecifiedAmount` to 0, which means this hook isn't modifying the counterpart token in the swap. +3. It creates a `BeforeSwapDelta` using these amounts. +4. It returns the original `amountIn`, the created `delta`, and 0 for `lpFeeOverride`. + +This basic example doesn't modify the swap parameters or introduce any fees. It demonstrates the minimal structure of a `beforeSwap` hook using `BeforeSwapDelta`. + +## Advanced Example: Implementing a Fee + +For a more practical use case, here's an example that implements a simple fee mechanism: + +```solidity +function beforeSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata +) external override returns (int256 amountIn, BeforeSwapDelta delta, uint24) { + // Determine if this is a swap for token0 or token1 + bool zeroForOne = params.zeroForOne; + + // Convert the specified amount to int128, ensuring it's positive + int128 specifiedAmount = params.amountSpecified.abs().toInt128(); + + // Calculate a 0.1% fee + int128 fee = specifiedAmount / 1000; + + // Adjust the specified amount based on swap direction + int128 adjustedSpecifiedAmount; + if (params.exactInput) { + // For exact input, reduce the amount by the fee + adjustedSpecifiedAmount = specifiedAmount - fee; + } else { + // For exact output, increase the amount by the fee + adjustedSpecifiedAmount = specifiedAmount + fee; + } + + // Create the BeforeSwapDelta + delta = zeroForOne + ? BeforeSwapDelta.from(-adjustedSpecifiedAmount, 0) + : BeforeSwapDelta.from(0, -adjustedSpecifiedAmount); + + // Return the original amount as amountIn + amountIn = params.amountSpecified; + + // Return 0 for lpFeeOverride as we're not changing the LP fee + return (amountIn, delta, 0); +} +``` + +**Let's break down what this hook is doing:** + +1. **Swap Direction Determination:** + The hook checks `params.zeroForOne` to determine the direction of the swap (token0 to token1 or vice versa). +2. **Amount Conversion:** It converts `params.amountSpecified` to a positive `int128`. This is necessary because `BeforeSwapDelta` works with `int128` values. +3. **Fee Calculation:** A 0.1% fee is calculated based on the specified amount. +4. **Amount Adjustment:** Depending on whether the swap is exact input or exact output, the specified amount is adjusted: + - For exact input, the fee is subtracted (user provides less to the pool). + - For exact output, the fee is added (user needs to provide more to the pool). +5. **BeforeSwapDelta Creation:** The `BeforeSwapDelta` is created using the adjusted amount. The negative sign indicates that the pool will receive these tokens from the user. The amount is placed in either the first or second parameter of `BeforeSwapDelta.from()` depending on the swap direction. +6. **Return Values:** + - `amountIn` is set to the original `params.amountSpecified`. This allows the pool to account for the full amount the user is putting in or expecting out. + - The `delta` value contains our adjusted amounts. + - `0` is returned for `lpFeeOverride`, meaning we're not changing the default LP fee. + +**What This Accomplishes:** + +- This hook implements a 0.1% fee on the swaps. +- It handles both exact input and exact output swaps correctly. +- It accounts for the swap direction (token0 to token1 or vice versa). +- The fee is taken from the input amount for exact input swaps, or added to the input amount for exact output swaps. +- The pool will see the full input/output amount, but will only swap the adjusted amount (after accounting for the fee). +- The difference between the original amount and the adjusted amount effectively becomes the hook's fee. + +**Considerations:** + +- This example assumes the fee is always taken in the input token. In practice, you might want to design more sophisticated fee structures. +- The hook doesn't handle storage of collected fees. In a real implementation, you'd need to account for and possibly transfer these fees. +- Always ensure that your hook's logic is consistent with the overall pool behavior and doesn't introduce unexpected side effects. +- This implementation doesn't change the LP fee (lpFeeOverride is 0). In some cases, you might want to adjust this as well. + +As you can see from this example, by using `BeforeSwapDelta`, hooks can implement custom logic such as fees, rebates, or other modifications to the swap parameters, allowing for highly flexible and customizable pool behavior in Uniswap V4. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/types/currency.mdx b/docs/contracts/v4/reference/core/types/currency.mdx new file mode 100644 index 0000000000..376499b900 --- /dev/null +++ b/docs/contracts/v4/reference/core/types/currency.mdx @@ -0,0 +1,176 @@ +--- +title: Currency and CurrencyLibrary +--- + +`Currency` is a custom type that represents either native currency (ETH) or ERC20 tokens. + +## Type Definition + +```solidity +type Currency is address; +``` + +## Global Functions + +### equals + +```solidity +function equals(Currency currency, Currency other) pure returns (bool) +``` + +Checks if two `Currency` values are equal. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The first Currency value | +| other | Currency | The second Currency value | + +Returns `true` if the Currency values are equal, `false` otherwise. + +### greaterThan + +```solidity +function greaterThan(Currency currency, Currency other) pure returns (bool) +``` + +Compares two `Currency` values based on their underlying addresses. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The first Currency value | +| other | Currency | The second Currency value | + +Returns `true` if the underlying address of `currency` is numerically greater than the underlying address of `other`, `false` otherwise. + +Note: This comparison is based on the numerical value of the addresses and does not imply any inherent ordering or value relationship between different currencies. It's primarily used for consistent ordering in data structures. + +### lessThan + +```solidity +function lessThan(Currency currency, Currency other) pure returns (bool) +``` + +Compares two `Currency` values based on their underlying addresses. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The first Currency value | +| other | Currency | The second Currency value | + +Returns `true` if the underlying address of `currency` is numerically less than the underlying address of `other`, `false` otherwise. + +Note: As with `greaterThan`, this comparison is based on address values and does not imply any inherent ordering or value relationship between currencies. + +### greaterThanOrEqualTo + +```solidity +function greaterThanOrEqualTo(Currency currency, Currency other) pure returns (bool) +``` + +Checks if one `Currency` value is greater than or equal to another, based on their underlying addresses. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The first Currency value | +| other | Currency | The second Currency value | + +Returns `true` if the underlying address of `currency` is numerically greater than or equal to the underlying address of `other`, `false` otherwise. + +# CurrencyLibrary + +The `CurrencyLibrary` provides utility functions for handling both native currency (ETH) and ERC20 tokens. + +## Constants + +```solidity +Currency public constant NATIVE = Currency.wrap(address(0)); +``` + +`NATIVE` represents the native currency (ETH). It is defined as a `Currency` with the underlying address of `address(0)`. + +## Functions + +### transfer + +```solidity +function transfer(Currency currency, address to, uint256 amount) internal +``` + +Transfers `amount` of `currency` to the `to` address. + +| Param Name | Type | Description | +|------------|----------|---------------------------------------| +| currency | Currency | The currency to transfer | +| to | address | The recipient address | +| amount | uint256 | The amount of currency to transfer | + +### balanceOfSelf + +```solidity +function balanceOfSelf(Currency currency) internal view returns (uint256) +``` + +Returns the balance of `currency` held by the contract itself. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The currency to check | + +Returns the balance of the specified currency. + +### balanceOf + +```solidity +function balanceOf(Currency currency, address owner) internal view returns (uint256) +``` + +Returns the balance of `currency` held by the `owner` address. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The currency to check | +| owner | address | The address to check | + +Returns the balance of the specified currency for the given address. + +### isNative + +```solidity +function isNative(Currency currency) internal pure returns (bool) +``` + +Checks if the given `currency` is the native currency (ETH). + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The currency to check | + +Returns `true` if the currency is native (ETH), `false` otherwise. + +### toId + +```solidity +function toId(Currency currency) internal pure returns (uint256) +``` + +Converts a `Currency` to its corresponding ID. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| currency | Currency | The currency to convert | + +Returns the ID of the currency. + +### fromId + +```solidity +function fromId(uint256 id) internal pure returns (Currency) +``` + +Converts an ID to its corresponding `Currency`. + +| Param Name | Type | Description | +|------------|----------|----------------------------| +| id | uint256 | The ID to convert | + +Returns the Currency corresponding to the given ID. \ No newline at end of file diff --git a/docs/contracts/v4/reference/core/types/poolkey.mdx b/docs/contracts/v4/reference/core/types/poolkey.mdx new file mode 100644 index 0000000000..765b33f31f --- /dev/null +++ b/docs/contracts/v4/reference/core/types/poolkey.mdx @@ -0,0 +1,34 @@ +--- +title: PoolKey +--- + +`PoolKey` is a crucial struct in Uniswap V4 that uniquely identifies a liquidity pool. It encapsulates all the essential parameters that define a pool's characteristics. + +# Structure + +```solidity +struct PoolKey { + Currency currency0; + Currency currency1; + uint24 fee; + int24 tickSpacing; + IHooks hooks; +} +``` + +# Fields + +| Field Name | Type | Description | +|-------------|----------|----------------------------------------------------------------------------------------| +| currency0 | Currency | The lower currency of the pool, sorted numerically | +| currency1 | Currency | The higher currency of the pool, sorted numerically | +| fee | uint24 | The pool swap fee, capped at 1,000,000. If the first bit is 1, the pool has a dynamic fee | +| tickSpacing | int24 | The spacing between ticks for the pool | +| hooks | IHooks | The address of the hooks contract associated with the pool | + +# Important Notes + +- The `currency0` and `currency1` fields are always sorted numerically, with `currency0` being the lower value. This ensures consistent pool identification regardless of the order in which tokens are provided. +- The `fee` field can represent either a static fee or indicate that the pool uses a dynamic fee mechanism. +- The `tickSpacing` field determines the granularity of price ranges that can be used for liquidity provision. +- The `hooks` field is an interface of our Hooks that the PoolManager uses to call these functions \ No newline at end of file diff --git a/docs/contracts/v4/reference/periphery/_category_.json b/docs/contracts/v4/reference/periphery/_category_.json new file mode 100644 index 0000000000..bef23ca2dc --- /dev/null +++ b/docs/contracts/v4/reference/periphery/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Periphery", + "position": 2, + "collapsed": true +} diff --git a/docs/contracts/v4/reference/periphery/positionmanager.mdx b/docs/contracts/v4/reference/periphery/positionmanager.mdx new file mode 100644 index 0000000000..b278317628 --- /dev/null +++ b/docs/contracts/v4/reference/periphery/positionmanager.mdx @@ -0,0 +1,5 @@ +--- +title: PositionManager +--- + +... in progress, please see [mint position](../../guides/02-manage-liquidity/01-mint-position.mdx) \ No newline at end of file