Skip to main content
Helpful?

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.

    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.

    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" hook that uses the locking mechanism and calls the modifyPosition and swap functions.

/// @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.

/// @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);
}
}
Helpful?