5/5
## Understanding Uniswap v4's Flash Accounting for Liquidity Positions Minting a new liquidity position in Uniswap v4 introduces a powerful but nuanced mechanism known as "flash accounting." Unlike previous versions where token transfers happened immediately, v4 separates the creation of a position from the settlement of the required tokens, all within a single atomic transaction. When a user calls the `PositionManager` contract to mint a new position, the internal `_mint` function calls `modifyLiquidity` on the central `PoolManager` contract. Critically, this `modifyLiquidity` call does not immediately pull tokens from the user's wallet. Instead, it registers a temporary debt for the `PositionManager`, creating a "negative delta." This delta represents the amount of tokens the `PositionManager` owes the `PoolManager` to properly fund the new liquidity. This debt must be settled before the transaction completes. However, the `PositionManager` does not automatically resolve this negative delta. The responsibility falls to the initial caller, who must provide an explicit set of instructions on how to pay the debt. This design allows for incredible flexibility, enabling complex, multi-step operations to be bundled into one efficient transaction. ## How to Mint a New Position with the Actions System To resolve the negative delta and complete the minting process, you must use the `PositionManager`'s "actions" system. This system allows you to define a sequence of operations and their corresponding parameters, which are then encoded and passed in a single function call. According to the official Uniswap v4 documentation, a standard position mint requires at least two sequential actions: 1. **`Actions.MINT_POSITION`**: This action initiates the process by creating the position NFT and calculating the required token amounts. This is the step that generates the negative delta. 2. **`Actions.SETTLE_PAIR`**: This action follows immediately to pay the token debt created in the first step, thereby settling the account with the `PoolManager`. These actions are packed into a `bytes` array to be passed to the contract. ```solidity // Define the sequence of operations: // 1. MINT_POSITION - Creates the position and calculates token requirements // 2. SETTLE_PAIR - Provides the tokens needed bytes memory actions = abi.encodePacked( Actions.MINT_POSITION, Actions.SETTLE_PAIR ); ``` By bundling these commands, you instruct the `PositionManager` to first create the position and then immediately use the provided tokens to settle the resulting debt. ## The Technical Execution Flow of a Mint Transaction As an end-user, you do not call internal functions like `_mint` or `_settlePair` directly. Instead, you interact with a single entry point function in `PositionManager.sol`, passing the encoded actions and parameters. The primary entry point is the `modifyLiquidities` function. The encoded actions and their parameters are passed as the `unlockData` argument. ```solidity // PositionManager.sol function modifyLiquidities(bytes calldata unlockData, uint256 deadline) external payable isNotLocked { checkDeadline(deadline); _executeActions(unlockData); } ``` From here, the execution flow is managed by `BaseActionsRouter.sol`, a contract that `PositionManager` inherits from. The logic proceeds as follows: 1. The `_executeActions` call triggers an internal `unlockCallback` function. 2. `unlockCallback` decodes the `unlockData` into a `bytes` array of `actions` and a `bytes[]` array of `params`. 3. It then calls `_executeActionsWithoutUnlock`, which loops through each action provided. 4. Inside the loop, `_handleAction` is called for each action. This function acts as a large switch statement, routing the execution to the appropriate internal logic based on the current action type. ```solidity // BaseActionsRouter.sol - Simplified Logic function unlockCallback(bytes calldata data) internal virtual override returns (bytes memory) { (bytes memory actions, bytes[] memory params) = data.decodeActionsRouterData(); _executeActionsWithoutUnlock(actions, params); return; } ``` ## A Deep Dive into Mint and Settle Actions The `_handleAction` function is where the specific logic for each step is executed. Let's examine the two primary actions for minting a position. ### Action 1: `Actions.MINT_POSITION` When the `_handleAction` function encounters `Actions.MINT_POSITION`, it decodes the parameters required for minting—such as the pool key, tick range, and liquidity amount—and calls the internal `_mint` function. ```solidity // PositionManager.sol (in _handleAction) else if (action == Actions.MINT_POSITION) { ( PoolKey calldata poolKey, int24 tickLower, int24 tickUpper, uint256 liquidity, uint128 amount0Max, uint128 amount1Max, address owner, bytes calldata hookData ) = params.decodeMintParams(); _mint(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData); return; } ``` This call successfully creates the position NFT but leaves the `PositionManager` with a negative delta owed to the `PoolManager`. ### Action 2: `Actions.SETTLE_PAIR` Next in the sequence, `_handleAction` processes `Actions.SETTLE_PAIR`. This action's sole purpose is to resolve the debt created by the previous step. ```solidity // PositionManager.sol (in _handleAction) else if (action == Actions.SETTLE_PAIR) { (Currency currency0, Currency currency1) = params.decodeCurrencyPair(); _settlePair(currency0, currency1); return; } ``` This calls `_settlePair`, which then calls the `_settle` function (from `DeltaResolver.sol`) for each currency in the pair. The `_settle` function finalizes the accounting by transferring the tokens. It calls `poolManager.sync(currency)` to get the latest delta and then triggers the actual payment via an internal `_pay` function. The `_pay` function in `PositionManager.sol` uses `permit2.transferFrom` to pull the required ERC20 tokens from the user's address directly to the `PoolManager`, thus settling the debt and completing the minting process. ```solidity // PositionManager.sol // implementation of abstract function DeltaResolver._pay function _pay(Currency currency, address payer, uint256 amount) internal override { if (payer == address(this)) { currency.transfer(address(poolManager), amount); } else { // Casting from uint256 to uint160 is safe due to limits on the total supply of a token permit2.transferFrom(payer, address(poolManager), uint160(amount), currency.unwrap()); } } ``` ## Minting Positions with ETH and Handling Refunds The process requires a special consideration when one of the tokens is a native currency like ETH. Since the `PositionManager` contract cannot "pull" ETH from a user's wallet, the user must proactively send the required ETH along with the transaction. This is why the `modifyLiquidities` function is marked as `payable`. This creates a new challenge: the exact amount of ETH required is only calculated *after* the `MINT_POSITION` action has executed. Consequently, a user often needs to send a slightly higher amount of ETH than necessary to ensure the transaction doesn't fail due to insufficient funds. Any excess ETH is then left in the `PositionManager` contract's balance. To solve this, Uniswap v4 provides a third action: `SWEEP`. This action instructs the `PositionManager` to transfer any remaining balance of a specified currency back to a designated address. ```solidity // PositionManager.sol function _sweep(Currency currency, address to) internal { uint256 balance = currency.balanceOfSelf(); if (balance > 0) currency.transfer(to, balance); } ``` Therefore, the complete sequence for minting a position involving ETH typically includes three actions to ensure any overpayment is refunded: 1. `MINT_POSITION` 2. `SETTLE_PAIR` 3. `SWEEP`
Minting a new liquidity position in Uniswap v4 introduces a powerful but nuanced mechanism known as "flash accounting." Unlike previous versions where token transfers happened immediately, v4 separates the creation of a position from the settlement of the required tokens, all within a single atomic transaction.
When a user calls the PositionManager contract to mint a new position, the internal _mint function calls modifyLiquidity on the central PoolManager contract. Critically, this modifyLiquidity call does not immediately pull tokens from the user's wallet. Instead, it registers a temporary debt for the PositionManager, creating a "negative delta." This delta represents the amount of tokens the PositionManager owes the PoolManager to properly fund the new liquidity.
This debt must be settled before the transaction completes. However, the PositionManager does not automatically resolve this negative delta. The responsibility falls to the initial caller, who must provide an explicit set of instructions on how to pay the debt. This design allows for incredible flexibility, enabling complex, multi-step operations to be bundled into one efficient transaction.
To resolve the negative delta and complete the minting process, you must use the PositionManager's "actions" system. This system allows you to define a sequence of operations and their corresponding parameters, which are then encoded and passed in a single function call.
According to the official Uniswap v4 documentation, a standard position mint requires at least two sequential actions:
Actions.MINT_POSITION: This action initiates the process by creating the position NFT and calculating the required token amounts. This is the step that generates the negative delta.
Actions.SETTLE_PAIR: This action follows immediately to pay the token debt created in the first step, thereby settling the account with the PoolManager.
These actions are packed into a bytes array to be passed to the contract.
By bundling these commands, you instruct the PositionManager to first create the position and then immediately use the provided tokens to settle the resulting debt.
As an end-user, you do not call internal functions like _mint or _settlePair directly. Instead, you interact with a single entry point function in PositionManager.sol, passing the encoded actions and parameters.
The primary entry point is the modifyLiquidities function. The encoded actions and their parameters are passed as the unlockData argument.
From here, the execution flow is managed by BaseActionsRouter.sol, a contract that PositionManager inherits from. The logic proceeds as follows:
The _executeActions call triggers an internal unlockCallback function.
unlockCallback decodes the unlockData into a bytes array of actions and a bytes[] array of params.
It then calls _executeActionsWithoutUnlock, which loops through each action provided.
Inside the loop, _handleAction is called for each action. This function acts as a large switch statement, routing the execution to the appropriate internal logic based on the current action type.
The _handleAction function is where the specific logic for each step is executed. Let's examine the two primary actions for minting a position.
Actions.MINT_POSITIONWhen the _handleAction function encounters Actions.MINT_POSITION, it decodes the parameters required for minting—such as the pool key, tick range, and liquidity amount—and calls the internal _mint function.
This call successfully creates the position NFT but leaves the PositionManager with a negative delta owed to the PoolManager.
Actions.SETTLE_PAIRNext in the sequence, _handleAction processes Actions.SETTLE_PAIR. This action's sole purpose is to resolve the debt created by the previous step.
This calls _settlePair, which then calls the _settle function (from DeltaResolver.sol) for each currency in the pair. The _settle function finalizes the accounting by transferring the tokens. It calls poolManager.sync(currency) to get the latest delta and then triggers the actual payment via an internal _pay function.
The _pay function in PositionManager.sol uses permit2.transferFrom to pull the required ERC20 tokens from the user's address directly to the PoolManager, thus settling the debt and completing the minting process.
The process requires a special consideration when one of the tokens is a native currency like ETH. Since the PositionManager contract cannot "pull" ETH from a user's wallet, the user must proactively send the required ETH along with the transaction. This is why the modifyLiquidities function is marked as payable.
This creates a new challenge: the exact amount of ETH required is only calculated after the MINT_POSITION action has executed. Consequently, a user often needs to send a slightly higher amount of ETH than necessary to ensure the transaction doesn't fail due to insufficient funds. Any excess ETH is then left in the PositionManager contract's balance.
To solve this, Uniswap v4 provides a third action: SWEEP. This action instructs the PositionManager to transfer any remaining balance of a specified currency back to a designated address.
Therefore, the complete sequence for minting a position involving ETH typically includes three actions to ensure any overpayment is refunded:
MINT_POSITION
SETTLE_PAIR
SWEEP
A transactional breakdown to Understanding Uniswap v4's Flash Accounting for Liquidity Positions - Explore the "create now, pay later" mechanism that separates position creation from token settlement, and master the actions system used to atomically resolve the resulting debt.
Previous lesson
Previous
Next lesson
Next
Course Overview
About the course
Difference between Uniswap v3 and v4
Uniswap v4 PoolManager
Uniswap v4 Hooks
Uniswap v4 PositionManager
Uniswap v4 Universal Router
Uniswap v4 Singleton architecture and flash accounting
Uniswap v4 operations and lifecycle
Uniswap v4 multihopping and quoting
How to build a Uniswap v4 swap router
How to build a smart contract a liquidation bot executes
Last updated on November 6, 2025
Duration: 5min
Duration: 7min
Duration: 39min
Duration: 35min
Duration: 34min
Duration: 31min
Course Overview
About the course
Difference between Uniswap v3 and v4
Uniswap v4 PoolManager
Uniswap v4 Hooks
Uniswap v4 PositionManager
Uniswap v4 Universal Router
Uniswap v4 Singleton architecture and flash accounting
Uniswap v4 operations and lifecycle
Uniswap v4 multihopping and quoting
How to build a Uniswap v4 swap router
How to build a smart contract a liquidation bot executes
Last updated on November 6, 2025