Omniscia Steer Protocol Audit

PoolSharkMultiPositionLiquidityManager Manual Review Findings

PoolSharkMultiPositionLiquidityManager Manual Review Findings

PSM-01M: Potential Denial-of-Service of Liquidity Manager (Poolshark Integration)

Description:

The Poolshark AMM will burn an NFT ID if its position is withdrawn in full. As such, a PoolSharkMultiPositionLiquidityManager::_burnAndCollect function call that would result in all shares being burned would cause the NFT ID to be burned and thus to cause several functions of the system inoperable, such as PoolSharkMultiPositionLiquidityManager::getTotalAmounts, due to relying on proper positionIds values.

Impact:

While the documentation acknowledges this flaw and specifies it should never happen, its consequences would be devastating to the operation of the PoolSharkMultiPositionLiquidityManager and as such we advise this trait to be prevented at the code level.

Example:

contracts/vault-types/PoolSharkLiquidityManagers/PoolSharkMultiPositionLiquidityManager.sol
381function _burnAndCollect(
382 uint256 shares,
383 uint256 totalShares
384) internal override returns (uint256 t0, uint256 t1) {
385 // First, fetch current positions, Only tend() and withdraw() call this function,
386 // and neither uses these positions elsewhere (tend uses updated ones).
387 LiquidityPositions[] memory _positions = positions;
388
389 // For each position, burn() and then withdraw correct amount of liquidity.
390 uint256 fees0;
391 uint256 fees1;
392 uint256 positionCount = _positions.length;
393 for (uint256 i; i != positionCount; ++i) {
394 //when called from tend shares and total shares are 1 and 1 so liquidityPercentageToBurn becomes 1e38 which just burning the whole position
395 //when called from withdraw shares should never be greater than total shares so liquidityPercentageToBurn <= 1e38
396 uint256 liquidityPercentageToBurn = FullMath.mulDiv(
397 1e38,
398 shares,
399 totalShares
400 );
401
402 // amountsOwed are always pulled with liquidity in this contract,
403 // so if position liquidity is 0, no need to withdraw anything.
404 if (liquidityPercentageToBurn > 0) {
405 // Amount burned in each position.
406
407 PoolsharkStructs.BurnRangeParams memory burnParams = PoolsharkStructs
408 .BurnRangeParams({
409 to: address(this),
410 positionId: positionIds[i],
411 burnPercent: 1 // passing 1 to just claim the fees
412 });
413 (int256 collect0, int256 collect1) = pool.burnRange(
414 burnParams
415 ); //@todo check that pssing 1 as burnPercent returns the claimed fees0 and fees1
416
417 // Corresponds to amount of liquidity withdrawn (i.e. doesn't include fees).
418 burnParams = PoolsharkStructs.BurnRangeParams({
419 to: address(this),
420 positionId: positionIds[i],
421 burnPercent: uint128(liquidityPercentageToBurn) // this should always be less than 1e38
422 });
423 (int256 posBurned0, int256 posBurned1) = pool.burnRange(
424 burnParams
425 );

Recommendation:

We advise the system to subtract 1 from the liquidityPercentageToBurn unless shares is equal to totalShares which in turn is equal to 1, in which case a burn operation should be performed per the PoolSharkMultiPositionLiquidityManager::_setBins function.

Alternatively, we advise the PoolSharkMultiPositionLiquidityManager::_burnAndCollect function to properly update the positionIds array if a position ID was burned permitting all operations to be performed safely.

Alleviation (6513a21a002d422e298719b22f73a4559dfd4663):

The code was updated to erase the positionIds when a full withdrawal is expected, properly maintaining the data points of the PoolSharkMultiPositionLiquidityManager contract and alleviating this exhibit.