From 578fbe89b22c24b434a68f99afdc5195622652f4 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 8 Oct 2024 21:42:03 +0200 Subject: [PATCH 01/14] added Reading Pool State guide --- .../v4/guides/07-read-pool-state.mdx | 256 +++++++++++++++++- 1 file changed, 253 insertions(+), 3 deletions(-) diff --git a/docs/contracts/v4/guides/07-read-pool-state.mdx b/docs/contracts/v4/guides/07-read-pool-state.mdx index 9c29a4cd1..d330b885c 100644 --- a/docs/contracts/v4/guides/07-read-pool-state.mdx +++ b/docs/contracts/v4/guides/07-read-pool-state.mdx @@ -2,8 +2,258 @@ title: Reading Pool State --- -## 🚧 Under construction 🚧 +## Introduction -Please see [StateLibrary](https://github.com/Uniswap/v4-core/blob/main/src/libraries/StateLibrary.sol) +Unlike previous versions, v4 uses a different approach for storing and accessing pool data, which requires understanding the use of `StateLibrary` and `extsload`. -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 +## Understanding the PoolManager Architecture + +### The Singleton Design + +In Uniswap v4, all pools are managed by a single `PoolManager` contract, unlike v3 where each pool was a separate contract. This design offers simplified management since all pools are now accessible through a single contract. + +This approach significantly reduces deployment costs, simplifies protocol upgrades, and enables more efficient cross-pool interactions. It also allows for easier implementation of new features across all pools simultaneously. + +### Pools as Library Calls + +In v4, pools are stored as complex structs, with Solidity libraries handling state changes. The `PoolManager` contract uses these libraries to perform operations on the pool state: + +```solidity +contract PoolManager { + using Pool for Pool.State; + mapping(PoolId => Pool.State) internal pools; + + function swap(PoolId id, ...) external { + pools[id].swap(...); // Library call + } +} +``` + +This design allows all AMM logic to be encapsulated within the `PoolManager` contract. + +## Reading Pool State in v4 + +In Uniswap v4, reading pool state involves a few key concepts and mechanisms that differ from previous versions. At the core of this new structure is a complex mapping within the PoolManager contract: + +```solidity +mapping(PoolId id => Pool.State) internal _pools; +``` + +This mapping represents a fundamental shift in pool data storage: + +1. Each pool is identified by a unique `PoolId`. +2. The `Pool.State` is a struct that contains all the state variables for a single pool. +3. This struct itself contains several nested mappings and complex data structures. + +For example, the `Pool.State` struct might look something like this (simplified for illustration): + +```solidity +struct State { + uint160 sqrtPriceX96; + int24 tick; + uint128 liquidity; + uint256 feeGrowthGlobal0X128; + uint256 feeGrowthGlobal1X128; + mapping(int24 => TickInfo) ticks; + mapping(bytes32 => Position.Info) positions; + // ... other fields +} +``` + +This complex structure allows for efficient storage of multiple pools and their associated data within a single contract. However, it also means that traditional getter functions would be inefficient or impractical for accessing this data, especially for nested mappings like `ticks` and `positions`. + +To address this, Uniswap V4 introduces the StateLibrary and the concept of using `extsload` for reading pool state. These mechanisms provide efficient ways to access the data stored in this complex structure. + +### The StateLibrary and `extsload` + +```solidity +abstract contract Extsload is IExtsload { + /// @inheritdoc IExtsload + function extsload(bytes32 slot) external view returns (bytes32) { + assembly ("memory-safe") { + mstore(0, sload(slot)) + return(0, 0x20) + } + } + + // [...] +} +``` + +The `StateLibrary` is a crucial component in Uniswap v4 for reading pool state. It utilizes the `extsload` function, which is an external wrapper for the `SLOAD` opcode. This allows for efficient reading of arbitrary storage slots. + +**How `extsload` works:** + +- It takes a storage slot as input. +- It reads the value stored in that slot directly, using `SLOAD`, from the contract's storage. +- It returns the value as a `bytes32`. + +This method is more gas-efficient than traditional getter functions, especially when reading multiple storage slots. + +Moreover, using `extsload` instead of hand-written Solidity view functions helps to keep the contract bytecode size down. This is particularly important for Uniswap v4, as the core contracts are already quite close to Ethereum's contract size limit. + +### TransientStateLibrary and `exttload` + +```solidity +abstract contract Extsload is IExtsload { + /// @inheritdoc IExtsload + function extsload(bytes32 slot) external view returns (bytes32) { + assembly ("memory-safe") { + mstore(0, sload(slot)) + return(0, 0x20) + } + } + + // [...] +} +``` + +While `StateLibrary` deals with persistent storage, `TransientStateLibrary` is used for handling transient storage. Transient storage, introduced in EIP-1153, is a way to store data that is only needed for the duration of a transaction, making it ideal for temporary data. + +It uses the `exttload` function, which is similar to `extsload`, but for transient storage; it is an external wrapper for the `TLOAD` opcode. + +## Implementing a `PoolStateReader` Contract + +Let's create a `PoolStateReader` contract that showcases different methods for reading pool state. For each function, we'll explain its purpose, how it works, and provide an example use case. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; + +contract PoolStateReader { + using PoolIdLibrary for PoolKey; + + IPoolManager public immutable poolManager; + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } + + // Functions will be implemented here +} +``` + +Before we start, we need to import `StateLibrary` from the libraries available in v4-core. + +```jsx +import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; +``` + +Let's focus on this important line that we should add: + +```jsx +using StateLibrary for IPoolManager; +``` + +This line is crucial for our PoolStateReader contract because it allows us to call StateLibrary functions as if they were methods of the IPoolManager interface, like for instance now we will be able to do `poolManager.getSlot0()`. + +Now we’re up for breaking down each wrapper function that we're going to be adding to our helper contract, explain the purpose of the pool manager function to read the state, and provide use cases to make sure we understand its utility: + +### `getSlot0()` + +```solidity +function getPoolState(PoolKey calldata key) external view returns ( + uint160 sqrtPriceX96, + int24 tick, + uint24 protocolFee, + uint24 lpFee +) { + return poolManager.getSlot0(key.toId()); +} +``` + +**Explanation:** + +This function retrieves the current state of the pool, including its price, tick, and fee settings. It uses the `getSlot0()` function from `StateLibrary`, which efficiently reads these values from a single storage slot. + +- `sqrtPriceX96`: The current price, encoded as a square root and scaled by 2^96. This encoding allows for efficient price calculations in the Uniswap algorithm. +- `tick`: The current tick, representing the quantized price. Ticks are used to efficiently track and update liquidity positions. +- `protocolFee`: The current protocol fee, represented in hundredths of a bip (i.e., units of 0.0001%). +- `lpFee`: The current liquidity provider fee, also represented in hundredths of a bip. + +**Use Case:** + +This function is essential for any application that needs to know the current state of a Uniswap v4 pool. For example: + +- A price oracle could use this to get the current price of the pool. +- A trading bot could use this to determine if a trade is profitable given the current price and fees. +- A liquidity management system could use the `tick` to decide where to place new liquidity. + +### `getLiquidity()` + +```solidity +function getPoolLiquidity(PoolKey calldata key) external view returns (uint128 liquidity) { + return poolManager.getLiquidity(key.toId()); +} +``` + +**Explanation:** + +This function retrieves the current total liquidity in the pool. Liquidity in Uniswap v4 represents the amount of assets available for trading within the current price range. + +**Use Case:** + +Understanding the total liquidity is crucial for several scenarios: + +- Assessing the depth of the market and potential slippage for large trades. +- Monitoring the depth and growth of a pool over time. + +### `getPositionInfo` + +```solidity +function getPositionInfo( + PoolKey calldata key, + address owner, + int24 tickLower, + int24 tickUpper, + bytes32 salt +) external view returns ( + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128 +) { + return poolManager.getPositionInfo(key.toId(), owner, tickLower, tickUpper, bytes32(salt)); +} +``` + +**Explanation:** + +This function retrieves information about a specific liquidity position. It returns: + +- `liquidity`: The amount of liquidity in the position. +- `feeGrowthInside0LastX128` and `feeGrowthInside1LastX128`: The last recorded cumulative fees earned per unit of liquidity for each token. + +**Use Case:** + +This function is crucial for applications that need to manage or analyze individual liquidity positions: + +- A liquidity management dashboard could use this to display a user's current positions and earned fees. +- An automated liquidity provision system could use this to decide when to rebalance or compound rewards. +- An analytics tool could use this to calculate the performance of different liquidity provision strategies. + +### `getFeeGrowthGlobal` + +```solidity +function getFeeGrowthGlobal(PoolKey calldata key) external view returns ( + uint256 feeGrowthGlobal0X128, + uint256 feeGrowthGlobal1X128 +) { + return poolManager.getFeeGrowthGlobal(key.toId()); +} +``` + +**Explanation:** + +This function retrieves the global fee growth for both tokens in the pool. These values represent the cumulative fees per unit of liquidity since the pool's inception. + +**Use Case:** + +Global fee growth is essential for several advanced operations: + +- Calculating the fees earned by a position that has been held for a long time. +- Analyzing the overall fee generation of a pool over its lifetime. +- Comparing the performance of different pools or fee tiers. \ No newline at end of file From a5def1b63c3706a7f5e5b35dca4a65d943cfc6c3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 10 Oct 2024 08:33:23 +0200 Subject: [PATCH 02/14] small changes to read pool state --- .../contracts/v4/guides/07-read-pool-state.mdx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/contracts/v4/guides/07-read-pool-state.mdx b/docs/contracts/v4/guides/07-read-pool-state.mdx index d330b885c..bdb08fc3f 100644 --- a/docs/contracts/v4/guides/07-read-pool-state.mdx +++ b/docs/contracts/v4/guides/07-read-pool-state.mdx @@ -4,7 +4,7 @@ title: Reading Pool State ## Introduction -Unlike previous versions, v4 uses a different approach for storing and accessing pool data, which requires understanding the use of `StateLibrary` and `extsload`. +Unlike previous versions, v4 uses a different approach for storing and accessing pool data, which requires understanding the use of [`StateLibrary`](/contracts/v4/reference/core/libraries/StateLibrary) and [`extsload`](/contracts/v4/reference/core/Extsload). ## Understanding the PoolManager Architecture @@ -90,7 +90,7 @@ The `StateLibrary` is a crucial component in Uniswap v4 for reading pool state. This method is more gas-efficient than traditional getter functions, especially when reading multiple storage slots. -Moreover, using `extsload` instead of hand-written Solidity view functions helps to keep the contract bytecode size down. This is particularly important for Uniswap v4, as the core contracts are already quite close to Ethereum's contract size limit. +Moreover, using `extsload` instead of hand-written Solidity view functions lowers the contract bytecode size. This optimization is particularly important for Uniswap v4, as the core contracts are nearly at Ethereum's contract size limit. ### TransientStateLibrary and `exttload` @@ -108,9 +108,9 @@ abstract contract Extsload is IExtsload { } ``` -While `StateLibrary` deals with persistent storage, `TransientStateLibrary` is used for handling transient storage. Transient storage, introduced in EIP-1153, is a way to store data that is only needed for the duration of a transaction, making it ideal for temporary data. +While `StateLibrary` deals with persistent storage, [`TransientStateLibrary`](/contracts/v4/reference/core/libraries/transient-state-library) is used for handling transient storage. Transient storage, introduced in EIP-1153, is a way to store data that is only needed for the duration of a transaction, making it ideal for temporary data. -It uses the `exttload` function, which is similar to `extsload`, but for transient storage; it is an external wrapper for the `TLOAD` opcode. +It uses the [`exttload`](/contracts/v4/reference/core/Exttload) function, which is similar to `extsload`, but for transient storage; it is an external wrapper for the `TLOAD` opcode. ## Implementing a `PoolStateReader` Contract @@ -139,13 +139,13 @@ contract PoolStateReader { Before we start, we need to import `StateLibrary` from the libraries available in v4-core. -```jsx +```solidity import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; ``` Let's focus on this important line that we should add: -```jsx +```solidity using StateLibrary for IPoolManager; ``` @@ -256,4 +256,8 @@ Global fee growth is essential for several advanced operations: - Calculating the fees earned by a position that has been held for a long time. - Analyzing the overall fee generation of a pool over its lifetime. -- Comparing the performance of different pools or fee tiers. \ No newline at end of file +- Comparing the performance of different pools or fee tiers. + +--- + +For additional reference, see [`StateLibrary`](/contracts/v4/reference/core/libraries/StateLibrary) and [`Extsload`](/contracts/v4/reference/core/Extsload) \ No newline at end of file From e324cb925d62e40d3291d13b441e9640ae09404f Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 22 Oct 2024 14:20:34 +0200 Subject: [PATCH 03/14] Adding Custom Accounting guide --- .../v4/guides/08-custom-accounting.mdx | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/contracts/v4/guides/08-custom-accounting.mdx diff --git a/docs/contracts/v4/guides/08-custom-accounting.mdx b/docs/contracts/v4/guides/08-custom-accounting.mdx new file mode 100644 index 000000000..9a35b0820 --- /dev/null +++ b/docs/contracts/v4/guides/08-custom-accounting.mdx @@ -0,0 +1,235 @@ +# Introduction + +Uniswap v4 introduces a set of powerful, interconnected features that proposes a new way automated market makers (AMMs) can function. Custom accounting, hook fees, custom curves, and return deltas might seem like distinct concepts, but they form a cohesive system that enables unprecedented flexibility in decentralized exchange mechanisms. + +These features are grouped together because they collectively represent the core of Uniswap v4’s customizability. They all relate to how pool state is managed and modified, working in tandem to allow developers to create highly tailored AMM experiences. From dynamic fee structures to unique bonding curves. + +# Brief Overview of Concepts + +Before we dive into the details of custom accounting, hook fees, custom curves, and return deltas, let’s explore how these features work in Uniswap v4. + +## Delta Accounting in v4 + +As described in [Flash Accounting](https://docs.uniswap.org/contracts/v4/concepts/flash-accounting) Uniswap v4 tracks net token transfers with transient storage. Unlike previous versions that tracked absolute token balances, v4 records changes (*deltas*) to these balances. This approach is at the core of v4’s enhanced flexibility and efficiency. + +In the v4 architecture, the `PoolManager` manages credits or debits per address. After a swap router contract interacts with the PoolManager, the core contract determines that the swap router owes input tokens and must claim output tokens. Token balances are tracked as accumulated deltas in transient storage; and only the final deltas incur token transfers + +Delta accounting provides several key benefits: + +1. More efficient state management, especially for complex operations involving multiple steps. +2. Easier integration with hooks, allowing for custom logic to be applied to state changes. +3. Improved gas efficiency for many operations, as it reduces the number of storage writes. + +This system forms the foundation upon which other v4 features, such as hook fees and custom curves, are built. It allows for more complex pool behaviors while maintaining efficiency and flexibility. + +## Hook Fees in v4 + +Hook fees are a feature in Uniswap v4 that allow hook developers to monetize their hooks or implement custom value distribution mechanisms. Unlike pool fees or dynamic fees, hook fees are entirely separate and are implemented through custom logic in the hook itself. + +Key characteristics of hook fees in Uniswap v4: + +**Separate from Pool Fees** + +Hook fees are distinct from the standard pool fees. They can be implemented alongside pool fees without interfering with the core fee structure. + +**Implemented in beforeSwap** + +Hook fees are typically calculated and applied in the `beforeSwap` function, allowing them to modify the swap parameters before the core swap logic is executed. + +**Use of BeforeSwapDelta** + +Hook fees often utilize the `BeforeSwapDelta` mechanism to adjust swap amounts and transfer deltas from the hook to the swap router, enabling precise control over how the fee affects the swap. + +**Flexible Implementation** + +Developers have full control over how hook fees are calculated, collected, and distributed. This allows for complex fee structures tailored to specific use cases. In other words, developers can implement static fees, percentage-based fees, or even a fee that changes. + +**Potential Use Cases** + +- Monetization of hook development +- Implementation of withdrawal penalties (e.g., to penalize just-in-time liquidity provision) +- Custom value distribution for liquidity providers + +Keep reading because at the bottom we are providing a step by step guide on how to implement hook fees. + +## Custom Curves in v4 + +Custom Curves in Uniswap v4 represent a big change in AMM design flexibility. Unlike [Uniswap v2](https://docs.uniswap.org/contracts/v2/concepts/protocol-overview/how-uniswap-works) where the x*y=k formula was hardcoded, v4 allows developers to implement a wide variety of pricing models. + +This is made possible through the hook system, particularly hooks that can interact with the swap process. Custom curves allow developers to eject the native concentrated liquidity pricing mechanism. These hooks can intercept swap requests, apply custom pricing logic, and return modified swap parameters. This enables the creation of pools with unique characteristics, such as: + +- Stable asset pairs with minimal price impact +- Curves for special token types like rebasing tokens, RWAs, vault tokens + +For example, creating a custom curve for a stable swap pool would involve designing a pricing function that maintains tighter price ranges when assets are near parity. This could be achieved by implementing a curve that's flatter in the middle (where assets are at their expected 1:1 ratio) and steeper at the edges (to discourage large imbalances). + +This type of custom curve could significantly improve capital efficiency for stable asset pairs, reducing slippage for traders and potentially attracting more liquidity to the pool. It showcases how Uniswap v4's flexible architecture allows for tailored solutions to specific trading scenarios, opening up new possibilities in decentralized exchange design. + +## Return Deltas in v4 + +Return deltas are a fundamental mechanism in Uniswap v4's custom accounting system. They allow for precise, programmatic adjustments to the outcomes of operations within the protocol. + +Key aspects of return deltas: + +1. **Dual Adjustment**: Return deltas simultaneously modify the balance changes (deltas) for both the hook contract and the swap router. This dual nature ensures that custom logic is accurately reflected across the entire system. +2. **Credits and Debts Modification**: By adjusting these deltas, return deltas effectively alter the credits and debts owed by the hook and the swap router. This allows for complex economic models to be implemented directly within the protocol. +3. **Native Pricing Bypass**: Return deltas enable hooks to implement custom curves that can completely bypass Uniswap's native pricing mechanism. This opens up possibilities for entirely new types of automated market makers within the Uniswap ecosystem. +4. **Hook Fee Implementation**: Through return deltas, hooks can implement their own fee structures, separate from and additional to the core protocol fees. + +In essence, it allow for bespoke modification of operation results and enables features that were previously impossible in earlier versions of the protocol. + +# Implementing Hook Fees: A Step-by-Step Guide + +In this guide, we'll walk through the process of implementing a custom fee hook in Uniswap v4. We'll not only show you how to write the code but also explain what's happening under the hood at each step. + +## Step 1: Setting Up the Hook + +First, let's create our basic hook structure: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {BaseHook} from "v4-periphery/src/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 {Currency} from "v4-core/src/types/Currency.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol"; + +contract HookFeeExample is BaseHook { + uint256 public constant HOOK_FEE_PERCENTAGE = 10;// 0.01% fee + uint256 public constant FEE_DENOMINATOR = 100000; + + constructor(IPoolManager _poolManager) BaseHook(_poolManager) {} + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: false, + beforeRemoveLiquidity: false, + afterAddLiquidity: false, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: false, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: true, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } +} + +``` + +Here, we're setting up our hook with a constant fee of 0.01% and enabling the `beforeSwap` and `beforeSwapReturnDelta` permissions. + +## Step 2: Implementing the beforeSwap Function + +Now, let's implement our `beforeSwap` function: + +```solidity +function beforeSwap( + address, + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata +) external override returns (bytes4, BeforeSwapDelta, uint24) { + // Implementation details will be explained in the following sub-steps +} + +``` + +### Step 2.1: Calculate the swap amount and fee + +We determine the absolute swap amount and calculate our fee based on it. + +```solidity +uint256 swapAmount = params.amountSpecified < 0 + ? uint256(-params.amountSpecified) + : uint256(params.amountSpecified); +uint256 feeAmount = (swapAmount * HOOK_FEE_PERCENTAGE) / FEE_DENOMINATOR; + +``` + +### Step 2.2: Collect the fee + +We use `poolManager.take` to collect the fee. This creates a debt for our hook in the specified currency. + +```solidity +Currency feeCurrency = params.zeroForOne ? key.currency0 : key.currency1; +poolManager.take(feeCurrency, address(this), feeAmount); + +``` + +### Step 2.3: Create the BeforeSwapDelta + +This is where the magic happens. We create a `BeforeSwapDelta` that represents our fee. The `toBeforeSwapDelta` function takes two parameters: + +- The specified delta: This is our fee amount. It's positive because we're adding it to the hook's balance. +- The unspecified delta: We set this to 0 as we're not affecting the other currency. + +```solidity +BeforeSwapDelta returnDelta = toBeforeSwapDelta( + int128(int256(feeAmount)),// Specified delta (fee amount) + 0// Unspecified delta (no change) +); + +``` + +### Step 2.4: Return values + +We return the function selector, our `returnDelta`, and 0 for the fee override. + +```solidity +return (BaseHook.beforeSwap.selector, returnDelta, 0); +``` + +## Step 3: Understanding the BeforeSwapDelta Mechanism + +Now, let's dive deeper into how the `BeforeSwapDelta` works and how it affects the overall swap process. + +1. **Initial State**: +Let's say a user wants to swap 100 USDC for USDT. Initially: + - Hook's delta: (0, 0) + - User's swap request: -100 USDC (negative because they're selling) + +2. **After Hook Execution**: +Our hook has taken a 1 USDC fee (assuming 1% for simplicity): + - Hook's delta: (-1 USDC, 0) // The hook now owes 1 USDC to the pool + - BeforeSwapDelta returned: (1 USDC, 0) // This will be added to the hook's delta and subtracted from the swap delta + +3. **PoolManager Processing**: +The PoolManager applies our `BeforeSwapDelta`: +The pool then swaps 99 USDC for, let's say, 98 USDT. + + ```solidity + amountToSwap = params.amountSpecified + hookDelta.getSpecifiedDelta(); + -99 USDC = -100 USDC + 1 USDC + ``` + +4. **Delta Resolution**: +The PoolManager then resolves the deltas: + + ```solidity + // Hook's new delta + newHookDelta = oldHookDelta + returnDelta + (0, 0) = (-1 USDC, 0) + (1 USDC, 0) + + // Swap delta for router + swapDelta = (-99 USDC, 98 USDT) - (1 USDC, 0) + = (-100 USDC, 98 USDT) + ``` + +5. **Final Outcome**: + - The hook's debt is cleared: It took 1 USDC as a fee, but "returned" it to the swap process. + - The router (on behalf of the user) must pay 100 USDC (original amount including fee) and receives 98 USDT. + +This process demonstrates how `BeforeSwapDelta` effectively "transfers" the hook's outstanding delta to the swap router, ensuring that the user pays the fee while the hook collects it, all within a single atomic transaction. + +## Conclusion + +By implementing hook fees this way, we've leveraged Uniswap v4's delta accounting system to create a seamless fee collection mechanism. This approach allows for complex fee structures and behaviors without disrupting the core swap process or requiring separate fee transfers. \ No newline at end of file From 6af997e02e5b6e3c8cfed5a36d3e990cf2644dc9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 31 Oct 2024 00:19:02 +0100 Subject: [PATCH 04/14] formatting --- .../v4/guides/08-custom-accounting.mdx | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/docs/contracts/v4/guides/08-custom-accounting.mdx b/docs/contracts/v4/guides/08-custom-accounting.mdx index 9a35b0820..c6b0ee608 100644 --- a/docs/contracts/v4/guides/08-custom-accounting.mdx +++ b/docs/contracts/v4/guides/08-custom-accounting.mdx @@ -1,5 +1,8 @@ -# Introduction +--- +title: Unlock Callback & Deltas +--- +# Introduction Uniswap v4 introduces a set of powerful, interconnected features that proposes a new way automated market makers (AMMs) can function. Custom accounting, hook fees, custom curves, and return deltas might seem like distinct concepts, but they form a cohesive system that enables unprecedented flexibility in decentralized exchange mechanisms. These features are grouped together because they collectively represent the core of Uniswap v4’s customizability. They all relate to how pool state is managed and modified, working in tandem to allow developers to create highly tailored AMM experiences. From dynamic fee structures to unique bonding curves. @@ -10,7 +13,7 @@ Before we dive into the details of custom accounting, hook fees, custom curves, ## Delta Accounting in v4 -As described in [Flash Accounting](https://docs.uniswap.org/contracts/v4/concepts/flash-accounting) Uniswap v4 tracks net token transfers with transient storage. Unlike previous versions that tracked absolute token balances, v4 records changes (*deltas*) to these balances. This approach is at the core of v4’s enhanced flexibility and efficiency. +As described in [Flash Accounting](/contracts/v4/concepts/flash-accounting) Uniswap v4 tracks net token transfers with transient storage. Unlike previous versions that tracked absolute token balances, v4 records changes to these balances (*deltas*). This approach is at the core of v4’s enhanced flexibility and efficiency. In the v4 architecture, the `PoolManager` manages credits or debits per address. After a swap router contract interacts with the PoolManager, the core contract determines that the swap router owes input tokens and must claim output tokens. Token balances are tracked as accumulated deltas in transient storage; and only the final deltas incur token transfers @@ -54,7 +57,7 @@ Keep reading because at the bottom we are providing a step by step guide on how ## Custom Curves in v4 -Custom Curves in Uniswap v4 represent a big change in AMM design flexibility. Unlike [Uniswap v2](https://docs.uniswap.org/contracts/v2/concepts/protocol-overview/how-uniswap-works) where the x*y=k formula was hardcoded, v4 allows developers to implement a wide variety of pricing models. +Custom Curves in Uniswap v4 represent a big change in AMM design flexibility. Unlike [Uniswap v2](/contracts/v2/concepts/protocol-overview/how-uniswap-works) where the x*y=k formula was hardcoded, v4 allows developers to implement a wide variety of pricing models. This is made possible through the hook system, particularly hooks that can interact with the swap process. Custom curves allow developers to eject the native concentrated liquidity pricing mechanism. These hooks can intercept swap requests, apply custom pricing logic, and return modified swap parameters. This enables the creation of pools with unique characteristics, such as: @@ -74,9 +77,9 @@ Key aspects of return deltas: 1. **Dual Adjustment**: Return deltas simultaneously modify the balance changes (deltas) for both the hook contract and the swap router. This dual nature ensures that custom logic is accurately reflected across the entire system. 2. **Credits and Debts Modification**: By adjusting these deltas, return deltas effectively alter the credits and debts owed by the hook and the swap router. This allows for complex economic models to be implemented directly within the protocol. 3. **Native Pricing Bypass**: Return deltas enable hooks to implement custom curves that can completely bypass Uniswap's native pricing mechanism. This opens up possibilities for entirely new types of automated market makers within the Uniswap ecosystem. -4. **Hook Fee Implementation**: Through return deltas, hooks can implement their own fee structures, separate from and additional to the core protocol fees. +4. **Hook Fee Implementation**: Through return deltas, hooks can implement their own fee structures, separate from the core protocol fees. -In essence, it allow for bespoke modification of operation results and enables features that were previously impossible in earlier versions of the protocol. +In essence, return deltas allow for bespoke modification of an operation's result -- enabling features that were previously impossible in earlier versions of the protocol. # Implementing Hook Fees: A Step-by-Step Guide @@ -138,7 +141,7 @@ function beforeSwap( IPoolManager.SwapParams calldata params, bytes calldata ) external override returns (bytes4, BeforeSwapDelta, uint24) { - // Implementation details will be explained in the following sub-steps + // Implementation details will be explained in the following sub-steps } ``` @@ -174,8 +177,8 @@ This is where the magic happens. We create a `BeforeSwapDelta` that represents o ```solidity BeforeSwapDelta returnDelta = toBeforeSwapDelta( - int128(int256(feeAmount)),// Specified delta (fee amount) - 0// Unspecified delta (no change) + int128(int256(feeAmount)), // Specified delta (fee amount) + 0 // Unspecified delta (no change) ); ``` @@ -193,7 +196,7 @@ return (BaseHook.beforeSwap.selector, returnDelta, 0); Now, let's dive deeper into how the `BeforeSwapDelta` works and how it affects the overall swap process. 1. **Initial State**: -Let's say a user wants to swap 100 USDC for USDT. Initially: +Let's say a user wants to swap 100 USDC for USDT - Hook's delta: (0, 0) - User's swap request: -100 USDC (negative because they're selling) @@ -203,26 +206,26 @@ Our hook has taken a 1 USDC fee (assuming 1% for simplicity): - BeforeSwapDelta returned: (1 USDC, 0) // This will be added to the hook's delta and subtracted from the swap delta 3. **PoolManager Processing**: -The PoolManager applies our `BeforeSwapDelta`: +The PoolManager applies our `BeforeSwapDelta` The pool then swaps 99 USDC for, let's say, 98 USDT. - ```solidity - amountToSwap = params.amountSpecified + hookDelta.getSpecifiedDelta(); - -99 USDC = -100 USDC + 1 USDC - ``` +```solidity +amountToSwap = params.amountSpecified + hookDelta.getSpecifiedDelta(); +-99 USDC = -100 USDC + 1 USDC +``` 4. **Delta Resolution**: The PoolManager then resolves the deltas: - ```solidity - // Hook's new delta - newHookDelta = oldHookDelta + returnDelta - (0, 0) = (-1 USDC, 0) + (1 USDC, 0) - - // Swap delta for router - swapDelta = (-99 USDC, 98 USDT) - (1 USDC, 0) - = (-100 USDC, 98 USDT) - ``` +```solidity +// Hook's new delta +newHookDelta = oldHookDelta + returnDelta +(0, 0) = (-1 USDC, 0) + (1 USDC, 0) + +// Swap delta for router +swapDelta = (-99 USDC, 98 USDT) - (1 USDC, 0) + = (-100 USDC, 98 USDT) +``` 5. **Final Outcome**: - The hook's debt is cleared: It took 1 USDC as a fee, but "returned" it to the swap process. From bce03cd9564a371d3338c04580cd1c31cc48a479 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Mon, 4 Nov 2024 13:36:55 -0500 Subject: [PATCH 05/14] Update docs/contracts/v4/guides/08-custom-accounting.mdx --- docs/contracts/v4/guides/08-custom-accounting.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/08-custom-accounting.mdx b/docs/contracts/v4/guides/08-custom-accounting.mdx index c6b0ee608..f8c39a0c1 100644 --- a/docs/contracts/v4/guides/08-custom-accounting.mdx +++ b/docs/contracts/v4/guides/08-custom-accounting.mdx @@ -1,5 +1,5 @@ --- -title: Unlock Callback & Deltas +title: Custom Accounting --- # Introduction From adfb4ca3ff7f861247b131089b00fcccf2aeeac8 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 7 Nov 2024 22:52:03 +0100 Subject: [PATCH 06/14] Adding Swap routing guide --- docs/contracts/v4/guides/09-swap-routing.mdx | 301 +++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 docs/contracts/v4/guides/09-swap-routing.mdx diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx new file mode 100644 index 000000000..3f2ca6bbf --- /dev/null +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -0,0 +1,301 @@ +--- +title: Swap routing +--- + +# Introduction to Universal Router for Uniswap v4 Swaps + +Uniswap v4 introduces a new architecture where all pools are managed by a single PoolManager contract. While the underlying architecture uses a callback system for swaps, developers can still use the Universal Router to execute swaps on v4 pools, just as you would for v2 or v3. + +## What is the Universal Router? + +The Universal Router is a flexible, gas-efficient contract designed to execute complex swap operations across various protocols, including Uniswap v4. It serves as an intermediary between users and the Uniswap v4 `PoolManager`, handling the intricacies of swap execution. + +While it’s technically possible to interact directly with the PoolManager contract for swaps, this approach is generally not recommended due to its complexity and potential inefficiencies. The Universal Router is designed to abstract away these complexities, providing a more straightforward and efficient method for executing swaps on v4 pools. + +## UniversalRouter command encoding + +The [Universal Router](https://docs.uniswap.org/contracts/universal-router/overview) uses a unique encoding system for its commands and inputs, which is crucial to understand when configuring it for v4 swaps. + +When calling `UniversalRouter.execute`, you provide two main parameters: + +1. `bytes commands`: A string of bytes where each byte represents a single command to be executed. +2. `bytes[] inputs`: An array of byte strings, each containing the encoded parameters for its corresponding command. + +The `commands[i]` byte corresponds to the `inputs[i]` parameters, allowing for a series of operations to be defined and executed in sequence. + +Each command is encoded as a single byte (`bytes1`) with a specific structure: + +``` +0 1 2 3 4 5 6 7 +┌─┬─┬───────────┐ +│f│r| command │ +└─┴─┴───────────┘ +``` + +- The first bit (`f`) is a flag that determines whether the command is allowed to revert without causing the entire transaction to fail. This enables partial execution of complex transactions. +- The second bit (`r`) is reserved for future use, providing flexibility for potential upgrades. +- The remaining 6 bits represent the specific command to be executed. + +# Configuring Universal Router for Uniswap v4 Swaps + +## Use Cases + +Developers might need to configure the Universal Router for swapping on Uniswap v4 pools in several scenarios: + +1. **Building a DEX aggregator**: If you’re creating a platform that finds the best rates across multiple DEXes, you’ll want to include Uniswap v4 pools in your options. +2. **Developing a trading bot**: Automated trading strategies often require the ability to execute swaps programmatically across various pools and versions. +3. **Creating a Dapp**: Many DeFi applications (lending platforms, yield aggregators, etc.) need to perform token swaps as part of their core functionality. + +This guide focuses on how to interact with Universal Router from an on-chain contract. + +## Step 1: Set Up the Project + +First, we need to set up our project and import the necessary dependencies. We’ll create a new Solidity contract for our example. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { UniversalRouter } from "@uniswap/universal-router/contracts/UniversalRouter.sol"; +import { Commands } from "@uniswap/universal-router/contracts/libraries/Commands.sol"; +import { IPoolManager } from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import { IV4Router } from "@uniswap/v4-periphery/contracts/interfaces/IV4Router.sol"; +import { Actions } from "@uniswap/v4-periphery/contracts/libraries/Actions.sol"; +import { IPermit2 } from "@uniswap/permit2/contracts/interfaces/IPermit2.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract Example { + using StateLibrary for IPoolManager; + + UniversalRouter public immutable router; + IPoolManager public immutable poolManager; + IPermit2 public immutable permit2; + + constructor(address _router, address _poolManager, address _permit2) { + router = UniversalRouter(_router); + poolManager = IPoolManager(_poolManager); + permit2 = IPermit2(_permit2); + } + + // We'll add more functions here +} +``` + +In this step, we’re importing the necessary contracts and interfaces: + +- `UniversalRouter`: This will be our main interface for executing swaps. It provides a flexible way to interact with various Uniswap versions and other protocols. +- `Commands`: This library contains the command definitions used by the UniversalRouter. +- `IPoolManager`: This interface is needed for interacting with Uniswap v4 pools. While we don't directly use it in our simple example, it's often necessary for more complex interactions with v4 pools. +- `IPermit2`: This interface allows us to interact with the Permit2 contract, which provides enhanced token approval functionality. +- `StateLibrary`: This provides optimized functions for interacting with the PoolManager’s state. By using `StateLibrary`, we can more efficiently read and manipulate pool states, which is crucial for many operations in Uniswap v4. + +**Important note:** + +To use the Universal Router imports, you'll need to set up remappings in your project. This can be done using a `remappings.txt` file: + +``` +@uniswap/universal-router/=lib/universal-router/ + +[...] +``` + +## Step 2: Implement Token Approval with Permit2 + +`UniversalRouter` integrates with [Permit2](https://github.com/Uniswap/permit2), to enable users to have more safety, flexibility, and control over their ERC20 token approvals. + +Before we can execute swaps, we need to ensure our contract can transfer tokens. We’ll implement a function to approve the Universal Router to spend tokens on behalf of our contract. + +Here, for testing purposes, we set up our contract to use Permit2 with the UniversalRouter: + +```solidity +function approveTokenWithPermit2( + address token, + uint160 amount, + uint48 expiration +) external { + IERC20(token).approve(address(permit2), type(uint256).max); + permit2.approve(token, address(router), amount, expiration); +} +``` + +This function first approves Permit2 to spend the token, then uses Permit2 to approve the UniversalRouter with a specific amount and expiration time. + +## Step 3: Implementing a Swap Function + +### 3.1: Function Signature + +First, let’s define our function signature: + +```solidity +function swapExactInputSingle( + PoolKey calldata key, + uint128 amountIn, + uint128 minAmountOut +) external returns (uint256 amountOut) { + // Implementation will follow +} +``` + +The function takes: + +- `key`: The PoolKey struct that identifies the v4 pool +- `amountIn`: The exact amount of tokens to swap +- `minAmountOut`: The minimum amount of output tokens expected + +### 3.2: Encoding the Swap Command + +When encoding a swap command for the Universal Router, we need to choose between two types of swaps: + +1. Exact Input Swaps: + +Use this when you know the exact amount of tokens you want to swap in, and you're willing to accept any amount of output tokens above your minimum. This is common when you want to sell a specific amount of tokens. + +2. Exact Output Swaps: + +Use this when you need a specific amount of output tokens, and you're willing to spend up to a maximum amount of input tokens. This is useful when you need to acquire a precise amount of tokens, for example, to repay a loan or meet a specific requirement. + +Next, we encode the swap command: + +```solidity +bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP)); +``` + +Here, we're using `V4_SWAP`, which tells the Universal Router that we want to perform a swap on a Uniswap v4 pool. The specific type of swap (exact input or exact output) will be determined by the V4Router actions we encode later. As we saw earlier, we encode this as a single byte, which is how the Universal Router expects to receive commands. + +Check the complete list of [commands](https://docs.uniswap.org/contracts/universal-router/technical-reference#command). + +### 3.3: Action Encoding + +Now, let’s encode the actions for the swap: + +```solidity +// Encode V4Router actions +bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) +); +``` + +These actions define the sequence of operations that will be performed in our v4 swap: + +1. `SWAP_EXACT_IN_SINGLE`: This action specifies that we want to perform an exact input swap using a single pool. This is where we define the type of swap we discussed earlier. +2. `SETTLE_ALL`: This action ensures all input tokens involved in the swap are properly paid. This is part of v4's settlement pattern for handling token transfers. +3. `TAKE_ALL`: This final action collects all output tokens after the swap is complete. + +The sequence of these actions is important as they define the complete flow of our swap operation from start to finish. + +### 3.4: Preparing the Swap Inputs + +For our v4 swap, we need to prepare three parameters that correspond to our encoded actions: + +```solidity +bytes[] memory params = new bytes[](3); + +// First parameter: swap configuration +params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, // true if we're swapping token0 for token1 + amountIn: amountIn, // amount of tokens we're swapping + amountOutMinimum: minAmountOut, // minimum amount we expect to receive + sqrtPriceLimitX96: uint160(0), // no price limit set + hookData: bytes("") // no hook data needed + }) +); + +// Second parameter: specify input tokens for the swap +// encode SETTLE_ALL parameters +params[1] = abi.encode(key.currency0, amountIn); + +// Third parameter: specify output tokens from the swap +params[2] = abi.encode(key.currency1, minAmountOut); +``` + +Eachencoded parameter serves a specific purpose: + +1. The first parameter configures how the swap should be executed, defining the pool, amounts, and other swap-specific details +2. The second parameter defines what tokens we're putting into the swap +3. The third parameter defines what tokens we expect to receive from the swap + +These parameters work in conjunction with the actions we encoded earlier (`SWAP_EXACT_IN_SINGLE`, `SETTLE_ALL`, and `TAKE_ALL`) to execute our swap operation. + +### 3.5: Executing the Swap + +Now we can execute the swap using the Universal Router: + +```solidity +// Combine actions and params into inputs +inputs[0] = abi.encode(actions, params); + +// Execute the swap +router.execute(commands, inputs, block.timestamp); +``` + +This prepares and executes the swap based on our encoded commands, actions, and parameters. The `block.timestamp` deadline parameter ensures the transaction will be executed in the current block. + +### 3.6: Verifying the Swap Output + +After the swap, we need to verify that we received at least the minimum amount of tokens we specified: + +```solidity +amountOut = IERC20(key.currency1).balanceOf(address(this)); +require(amountOut >= minAmountOut, "Insufficient output amount"); +``` + +### 3.7: Returning the Result + +Finally, we return the amount of tokens we received: + +```solidity +return amountOut; +``` + +This allows the caller of the function to know exactly how many tokens were received in the swap. + +Here's the complete swap function that combines all the steps we've covered: + +```solidity +function swapExactInputSingle( + PoolKey calldata key, + uint128 amountIn, + uint128 minAmountOut +) external returns (uint256 amountOut) { +// Encode the Universal Router command + bytes memory commands = abi.encodePacked(uint8(Commands.V4_SWAP)); + bytes[] memory inputs = new bytes[](1); + +// Encode V4Router actions + bytes memory actions = abi.encodePacked( + uint8(Actions.SWAP_EXACT_IN_SINGLE), + uint8(Actions.SETTLE_ALL), + uint8(Actions.TAKE_ALL) + ); + +// Prepare parameters for each action + bytes[] memory params = new bytes[](3); + params[0] = abi.encode( + IV4Router.ExactInputSingleParams({ + poolKey: key, + zeroForOne: true, + amountIn: amountIn, + amountOutMinimum: minAmountOut, + sqrtPriceLimitX96: uint160(0), + hookData: bytes("") + }) + ); + params[1] = abi.encode(key.currency0, amountIn); + params[2] = abi.encode(key.currency1, minAmountOut); + +// Combine actions and params into inputs + inputs[0] = abi.encode(actions, params); + +// Execute the swap + router.execute(commands, inputs, block.timestamp); + +// Verify and return the output amount + amountOut = IERC20(key.currency1).balanceOf(address(this)); + require(amountOut >= minAmountOut, "Insufficient output amount"); + return amountOut; +} +``` \ No newline at end of file From dfdc902e94c8dfdb3439a98e45713e747ef8c1f1 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:45:39 -0500 Subject: [PATCH 07/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index 3f2ca6bbf..c5e573d20 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -234,7 +234,7 @@ router.execute(commands, inputs, block.timestamp); This prepares and executes the swap based on our encoded commands, actions, and parameters. The `block.timestamp` deadline parameter ensures the transaction will be executed in the current block. -### 3.6: Verifying the Swap Output +### 3.6: (Optional) Verifying the Swap Output After the swap, we need to verify that we received at least the minimum amount of tokens we specified: From b4b92e0b4d066de7dba4c36eb6c349f7a084d5d5 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:45:50 -0500 Subject: [PATCH 08/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index c5e573d20..950f5199f 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -14,7 +14,7 @@ While it’s technically possible to interact directly with the PoolManager cont ## UniversalRouter command encoding -The [Universal Router](https://docs.uniswap.org/contracts/universal-router/overview) uses a unique encoding system for its commands and inputs, which is crucial to understand when configuring it for v4 swaps. +The [Universal Router](/contracts/universal-router/overview) uses a unique encoding system for its commands and inputs, which is crucial to understand when configuring it for v4 swaps. When calling `UniversalRouter.execute`, you provide two main parameters: From 48a77a53d96ecc4c722f87ea44a1babc4ded3a0f Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:46:04 -0500 Subject: [PATCH 09/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index 950f5199f..aaff3bdc0 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -16,7 +16,7 @@ While it’s technically possible to interact directly with the PoolManager cont The [Universal Router](/contracts/universal-router/overview) uses a unique encoding system for its commands and inputs, which is crucial to understand when configuring it for v4 swaps. -When calling `UniversalRouter.execute`, you provide two main parameters: +When calling `UniversalRouter.execute()`, you provide two main parameters: 1. `bytes commands`: A string of bytes where each byte represents a single command to be executed. 2. `bytes[] inputs`: An array of byte strings, each containing the encoded parameters for its corresponding command. From dc3feffe05f2512e77bab2e5cac171e1be55d2fa Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:48:52 -0500 Subject: [PATCH 10/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index aaff3bdc0..ad46e4558 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -148,7 +148,7 @@ When encoding a swap command for the Universal Router, we need to choose between 1. Exact Input Swaps: -Use this when you know the exact amount of tokens you want to swap in, and you're willing to accept any amount of output tokens above your minimum. This is common when you want to sell a specific amount of tokens. + Use this swap-type when you know the exact amount of tokens you want to swap in, and you're willing to accept any amount of output tokens above your minimum. This is common when you want to sell a specific amount of tokens. 2. Exact Output Swaps: From f7e15890a7130f3fbdfdcff7be3bdacd260b0497 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:49:02 -0500 Subject: [PATCH 11/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index ad46e4558..5c081f6d4 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -152,7 +152,7 @@ When encoding a swap command for the Universal Router, we need to choose between 2. Exact Output Swaps: -Use this when you need a specific amount of output tokens, and you're willing to spend up to a maximum amount of input tokens. This is useful when you need to acquire a precise amount of tokens, for example, to repay a loan or meet a specific requirement. + Use this swap-type when you need a specific amount of output tokens, and you're willing to spend up to a maximum amount of input tokens. This is useful when you need to acquire a precise amount of tokens, for example, to repay a loan or meet a specific requirement. Next, we encode the swap command: From d836c4c4f023e8d45be6c639d76be29be42ab081 Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:49:15 -0500 Subject: [PATCH 12/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index 5c081f6d4..5ad5edda2 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -179,7 +179,7 @@ bytes memory actions = abi.encodePacked( These actions define the sequence of operations that will be performed in our v4 swap: -1. `SWAP_EXACT_IN_SINGLE`: This action specifies that we want to perform an exact input swap using a single pool. This is where we define the type of swap we discussed earlier. +1. `SWAP_EXACT_IN_SINGLE`: This action specifies that we want to perform an exact input swap using a single pool. 2. `SETTLE_ALL`: This action ensures all input tokens involved in the swap are properly paid. This is part of v4's settlement pattern for handling token transfers. 3. `TAKE_ALL`: This final action collects all output tokens after the swap is complete. From 111b07ec7ed0f2c5444603fdbb4696fb5de626de Mon Sep 17 00:00:00 2001 From: saucepoint <98790946+saucepoint@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:49:43 -0500 Subject: [PATCH 13/14] Update docs/contracts/v4/guides/09-swap-routing.mdx --- docs/contracts/v4/guides/09-swap-routing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contracts/v4/guides/09-swap-routing.mdx b/docs/contracts/v4/guides/09-swap-routing.mdx index 5ad5edda2..153d88e4e 100644 --- a/docs/contracts/v4/guides/09-swap-routing.mdx +++ b/docs/contracts/v4/guides/09-swap-routing.mdx @@ -212,7 +212,7 @@ params[1] = abi.encode(key.currency0, amountIn); params[2] = abi.encode(key.currency1, minAmountOut); ``` -Eachencoded parameter serves a specific purpose: +Each encoded parameter serves a specific purpose: 1. The first parameter configures how the swap should be executed, defining the pool, amounts, and other swap-specific details 2. The second parameter defines what tokens we're putting into the swap From b5650a29747d2c7c8d2978ed4d3333b59a6968ea Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 19 Nov 2024 17:41:49 +0100 Subject: [PATCH 14/14] ERC-6909 guide added --- docs/contracts/v4/guides/10-ERC-6909.mdx | 209 +++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/contracts/v4/guides/10-ERC-6909.mdx diff --git a/docs/contracts/v4/guides/10-ERC-6909.mdx b/docs/contracts/v4/guides/10-ERC-6909.mdx new file mode 100644 index 000000000..ad5e94a4e --- /dev/null +++ b/docs/contracts/v4/guides/10-ERC-6909.mdx @@ -0,0 +1,209 @@ +--- +title: ERC-6909 +--- + +# Introduction + +Uniswap v4 uses [ERC-6909](/contracts/v4/concepts/erc6909), a token standard that works alongside the protocol’s flash accounting system. This guide explains how ERC-6909 functions within v4, when to use mint versus burn operations, and how developers can implement them effectively. + +# What is ERC-6909? + +ERC-6909 is a token standard that enables efficient token management within a single contract through multiple token balances per user. Where ERC-20 requires separate approve and transfer calls for token interactions, ERC-6909 provides native support for multi-token operations through mint/burn mechanics that integrate with v4’s flash accounting system. + +Here’s how the approaches differ: + +```solidity +// Traditional ERC-20 approach +IERC20(tokenA).transferFrom(owner, poolManager, amount); + +// ERC-6909 approach in v4 +poolManager.burn(owner, currency.toId(), amount); +``` + +## Integration with Flash Accounting + +While flash accounting tracks balance changes as deltas throughout a transaction, ERC-6909 provides an additional primitive to resolve deltas. + +This enables: + +1. Simplified transaction flows through direct mint/burn operations +2. Efficient handling of multi-step operations +3. Streamlined token management when using the PoolManager + +## Gas Efficiency + +ERC-6909 provides gas savings compared to ERC-20 tokens, making it particularly valuable for use cases requiring frequent token transfers like: + +- Day trading operations +- MEV bot transactions +- Active liquidity management + +This efficiency is especially beneficial when performing multiple token operations in rapid succession. + +## Simplified Token Management + +The traditional ERC-20 workflow requires careful management of allowances and transfers, often leading to complex transaction sequences and potential security concerns. + +ERC-6909 takes a different approach by providing direct balance modifications through mint and burn operations. + +By working through the PoolManager, all token operations are consolidated into a single interface. This means you don’t need to worry about managing multiple token approvals or keeping track of allowances across different contracts. Instead, you can focus on the core logic of your application while the PoolManager handles the token management aspects. + +# Understanding ERC-6909 in v4 + +Let's explore how ERC-6909 is used across different v4 operations and understand when to use each of its operations. + +## Operations and Token Movement + +Different pool operations create different types of deltas that need to be resolved: + +- **Swaps**: Create negative deltas for input tokens and positive deltas for output tokens +- **Adding Liquidity**: Creates negative deltas (tokens you need to provide) +- **Removing Liquidity**: Creates positive deltas (tokens you receive) +- **Donations**: Creates negative deltas (tokens you're donating) + +## Using Mint and Burn + +The choice between mint and burn operations depends on your token movement needs: + +```solidity +// When you have positive deltas (withdrawing value from PoolManager): +poolManager.mint(currency, address(this), amount); + +// When you have negative deltas (transferring value to PoolManager): +poolManager.burn(currency, address(this), amount); +``` + +This pattern is used throughout v4's operations: + +- Use mint when withdrawing value from the pool (like receiving tokens from swaps) +- Use burn when transferring value to the pool (like providing tokens) + +## Hook Integration + +When building hooks, ERC-6909 operations help manage token movements within your hook's logic: + +```solidity +function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params) + external + returns (bytes4, BeforeSwapDelta, uint24) +{ + poolManager.mint(key.currency0, address(this), amount); + + return ( + BaseHook.beforeSwap.selector, + BeforeSwapDeltaLibrary.ZERO_DELTA, + 0 + ); +} +``` + +Other common cases would be to use `mint` for fee collection or `burn` for token distribution. + +# Implementation + +Let's build a contract that handles donations in v4 using ERC-6909. We'll create a donation router that follows this flow: + +1. Users call our donation function with their desired amounts +2. Our contract packages this data and uses the PoolManager's unlock pattern +3. In the callback, we unpack the data and execute the donation, handling token movements using ERC-6909 + +First, let's set up our contract with the necessary imports and create a struct to help us pass data between functions: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { IPoolManager } from "@uniswap/v4-core/contracts/interfaces/IPoolManager.sol"; +import { PoolKey } from "@uniswap/v4-core/contracts/types/PoolKey.sol"; +import { BalanceDelta } from "@uniswap/v4-core/contracts/types/BalanceDelta.sol"; +import { Currency } from "@uniswap/v4-core/contracts/types/Currency.sol"; + +contract DonationRouter { + IPoolManager public immutable poolManager; + + // This struct helps us pack donation parameters to pass through + // the unlock/callback pattern + struct CallbackData { + PoolKey key; + uint256 amount0; + uint256 amount1; + bytes hookData; + } + + constructor(IPoolManager _poolManager) { + poolManager = _poolManager; + } +} +``` + +Now let's implement the external donation function. Here we'll pack our parameters into the CallbackData struct and start the unlock process: + +```solidity +/// @notice Donates tokens to a pool +/// @param key The pool to donate to +/// @param amount0 Amount of token0 to donate +/// @param amount1 Amount of token1 to donate +/// @param hookData Optional data to pass to hooks +function donate( + PoolKey memory key, + uint256 amount0, + uint256 amount1, + bytes memory hookData +) external returns (BalanceDelta delta) { + // 1. Create a CallbackData struct with all our parameters + CallbackData memory data = CallbackData({ + key: key, + amount0: amount0, + amount1: amount1, + hookData: hookData + }); + + // 2. Encode our struct into bytes to pass through unlock + bytes memory encodedData = abi.encode(data); + + // 3. Call unlock with our encoded data + // 4. unlock will call our callback, which returns encoded delta + // 5. Decode the returned bytes back into a BalanceDelta + delta = abi.decode( + poolManager.unlock(encodedData), + (BalanceDelta) + ); +} +``` + +When the PoolManager calls our callback, we need to decode our data: + +```solidity +function unlockCallback( + bytes calldata rawData +) external returns (bytes memory) { + // Only the PoolManager can trigger our callback + require(msg.sender == address(poolManager)); + + // Decode the bytes back into our CallbackData struct + // (CallbackData) tells abi.decode what type to expect + CallbackData memory data = abi.decode(rawData, (CallbackData)); +``` + +Now `data` contains the same values we packed in donate(): + - `data.key`: The pool to donate to + - `data.amount0`: Amount of first token + - `data.amount1`: Amount of second token + - `data.hookData`: Any hook data + +And we can execute the donation: + +```solidity + // Execute the donation through PoolManager + // This creates negative deltas for the tokens we're donating + BalanceDelta delta = poolManager.donate( + data.key, + data.amount0, + data.amount1, + data.hookData + ); +``` + +After executing the donation through the PoolManager, we need to handle the token transfers. The donation creates negative deltas, which represent tokens that we owe to the PoolManager. This is where ERC-6909's burn operation comes into play. + +Instead of using traditional token transfers, we can use ERC-6909's burn operation to settle this debt. We check each token's delta and, if negative, burn the corresponding amount of ERC-6909 tokens. And finally return the encoded delta. Let’s see how: \ No newline at end of file