Liquidity provider loses Liquidity during collection initialization
Summary
The first liquidity provider initiating the collection through Locker::initializeCollection() loses ownership of their liquidity position due to how the UniswapImplementation::_unlockCallback() is executed. The owner of the liquidity position on Uniswap will incorrectly tracks UniswapImplementation as the owner of the liquidity.
File: UniswapImplementation.sol
376: function _unlockCallback(bytes calldata _data) internal override returns (bytes memory) {
377: // Unpack our passed data
378: CallbackData memory params = abi.decode(_data, (CallbackData));
379:
380: // As this call should only come in when we are initializing our pool, we
381: // don't need to worry about `take` calls, but only `settle` calls.
382:@> (BalanceDelta delta,) = poolManager.modifyLiquidity({
383: key: params.poolKey,
384: params: IPoolManager.ModifyLiquidityParams({
385: tickLower: MIN_USABLE_TICK,
386: tickUpper: MAX_USABLE_TICK,
387: liquidityDelta: int(uint(params.liquidityDelta)),
388: salt: ''
389: }),
390: hookData: ''
391: });
---
420: }
However, the v4-core/PoolManager uses msg.sender as the owner of the position.
In this case, UniswapImplementation contract becomes the owner of the liquidity position, not the user who initiated the Locker::initializeCollection().
As a result, the user who provided the initial liquidity has no control over the position they created.
Impact
The user who initiates the collection and provides the first liquidity will lose ownership of their liquidity position.
File: UniswapImplementation.sol
376: function _unlockCallback(bytes calldata _data) internal override returns (bytes memory) {
377: // Unpack our passed data
378: CallbackData memory params = abi.decode(_data, (CallbackData));
379:
380: // As this call should only come in when we are initializing our pool, we
381: // don't need to worry about `take` calls, but only `settle` calls.
382:@> (BalanceDelta delta,) = poolManager.modifyLiquidity({
383: key: params.poolKey,
384: params: IPoolManager.ModifyLiquidityParams({
385: tickLower: MIN_USABLE_TICK,
386: tickUpper: MAX_USABLE_TICK,
387: liquidityDelta: int(uint(params.liquidityDelta)),
388: salt: ''
389: }),
390: hookData: ''
391: });
---
420: }
File: Locker.sol
367: function initializeCollection(address _collection, uint _eth, uint[] calldata _tokenIds, uint _tokenSlippage, uint160 _sqrtPriceX96) public virtual whenNotPaused collectionExists(_collection) {
368: // Ensure the collection is not already initialised
---
384: nativeToken.transferFrom(msg.sender, address(_implementation), _eth);
385:
---
388: _implementation.initializeCollection(_collection, _eth, tokens, _tokenSlippage, _sqrtPriceX96);
389:
---
399: }
Tool used
Manual Review
Recommendation
Given that v4-core/PoolManager::modifyLiquidity() uses msg.sender as the owner and the Uniswap contract cannot be altered, consider the following strategies to address ownership concerns:
Use a PositionManager to handle liquidity positions for different users, ensuring users retain control.
Use the salt parameter to differentiate same-range positions, helping manage positions for multiple users.
Set up an external tracking system and facilitate liquidity withdrawal to verify and manage ownership for initial liquidity providers.
merlinboii
High
Liquidity provider loses Liquidity during collection initialization
Summary
The first liquidity provider initiating the collection through
Locker::initializeCollection()
loses ownership of their liquidity position due to how theUniswapImplementation::_unlockCallback()
is executed. The owner of the liquidity position on Uniswap will incorrectly tracksUniswapImplementation
as the owner of the liquidity.Vulnerability Detail
When a collection is initialized, the
UniswapImplementation::_unlockCallback()
is used to add the first liquidity to the pool.UniswapImplementation::_unlockCallback()
However, the
v4-core/PoolManager
usesmsg.sender
as the owner of the position.v4-core/tickbitmap-overload/PoolManager::modifyLiquidity()
In this case,
UniswapImplementation
contract becomes the owner of the liquidity position, not the user who initiated theLocker::initializeCollection()
.As a result, the user who provided the initial liquidity has no control over the position they created.
Impact
The user who initiates the collection and provides the first liquidity will lose ownership of their liquidity position.
Code Snippet
UniswapImplementation::_unlockCallback()
v4-core/tickbitmap-overload/PoolManager::modifyLiquidity()
UniswapImplementation::initializeCollection()
Locker::initializeCollection()
Tool used
Manual Review
Recommendation
Given that
v4-core/PoolManager::modifyLiquidity()
usesmsg.sender
as the owner and the Uniswap contract cannot be altered, consider the following strategies to address ownership concerns:Use a
PositionManager
to handle liquidity positions for different users, ensuring users retain control.Use the salt parameter to differentiate same-range positions, helping manage positions for multiple users.
Set up an external tracking system and facilitate liquidity withdrawal to verify and manage ownership for initial liquidity providers.