Panoptic - tapir's results

Effortless options trading on any token, any strike, any size.

General Information

Platform: Code4rena

Start Date: 27/11/2023

Pot Size: $60,500 USDC

Total HM: 7

Participants: 72

Period: 7 days

Judge: Picodes

Total Solo HM: 2

Id: 309

League: ETH

Panoptic

Findings Distribution

Researcher Performance

Rank: 6/72

Findings: 2

Award: $2,588.90

🌟 Selected for report: 1

πŸš€ Solo Findings: 0

Findings Information

🌟 Selected for report: tapir

Also found by: SpicyMeatball, hash

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
satisfactory
selected for report
sponsor confirmed
M-04

Awards

2446.1682 USDC - $2,446.17

External Links

Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L476-L533 https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L848-L1067 https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1200-L1329

Vulnerability details

Impact

When minting or burning a position attacker can take advantage of reentrancy to make the pool has more "removedLiquidity" than it has. This would make the owed premium to be mistakenly very high value. The issue root cause is reentrancy on an uninitialized pool in SFPM. SFPM initialize pool function explicitly makes the "locked" flag to false which can be leveraged for the attacker to reenter and burn the position before the mint goes through fully. This will lead to an underflow in the removedLiquidity part of the chunk. Thus, the premium owed will be calculated as a very big number since the removed liquidity is used to track the premiums owed.

Proof of Concept

It's best to explain this issue using an example due to its complexity:

First, the attacker is a contract that has a custom onERC1155Received implemented. In this case, the attacker checks for uninitialized pools in SFPM. Let's say the USDC-ETH 0.05% pool is not yet initialized. The attacker initiates a call to mintTokenizedPosition with a "short" type. Normally, this action would remove specific liquidity from Uniswapv3 deposited through SFPM. Attempting this without a deposit in that range would normally throw an error in this part of the code. Also, note that although the underlying uniswap pool is not registered mintTokenizedPosition does not reverts since the attacker will initialize the pool in the ERC1155 callback!

The attacker proceeds with the mint function despite expecting it to revert. The tokenId's Uniswap pool part (first 64 bits) remains uninitialized. The attacker takes advantage of this and, once the _mint is called internally in ERC1155, the attacker can execute their custom onERC1155Received in their contract.

As we can see here, before any execution on state and storage the _mint is called so attacker will has its onERC1155Received callback function called by SFPM: https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L521

Now, attacker will have the following onERC1155Received function:

function onERC1155Received(*/ address caller, address to */ , uint tokenId, uint amount, bytes calldata data) return bytes4 {
   SFPM.initializeAMMPool(USDC, WETH, FEE_TIER);
   SFPM.burnTokenizedPosition(tokenId, tokenId.positionSize(), slippageTickLimitLow, slippageTickLimitHigh);

   return ERC1155Holder.onERC1155Received.selector;
}

The first step here is initializing the pool. This action unlocks the pool's reentrancy lock, allowing re-entry into the functions for that pool link.

The second call burns the token. As this was a long position and has the "burn" flag, it flips the long to short link.

When reaching this part of the code, "isLong" equals 0, initiating an unchecked scope, resulting in an underflow of removedLiquidity, leading to an immensely large number!

Continuing with the function, these lines execute, minting liquidity using 1 ETH from the attacker, inadvertently increasing actual liquidity without a prior deposit.

The removedLiquidity and netLiquidity are then written to storage.

Despite the attacker's tokenId being burnt and nonexistent due to the onERC1155Received function execution, the process continues. Which means we can now continue with the initial mintTokenizedPosition function

Since the previous call updated removedLiquidity to an enormous number via underflow, and increasing netLiquidity with chunk liquidity, leads to no reversion in these lines.

Afterward, the position is successfully burned from Uniswap. Continuing with the function, at this point, the rightSlot holds the net liquidity from previous storage balance. Hence, we will execute the _collectAndWritePositionData function.

However, we have a problem arising in the _updateStoredPremia function which is called inside the _collectAndWritePositionData. link. Here, the removed liquidity is used to calculate the premium owed. Due to the underflow from previous storage balance in removedLiquidity (leftSlot), the resulting premiumOwed becomes immensely high. This impacts the premium owed to be an immensely big number, returning a false statement. Furthermore, users intending to use the USDC-ETH 0.05% pool may encounter difficulties due to messed-up premiums in one range position, potentially affecting other positions relying on these premiums within the external contract.

Tools Used

Manual

Validate the uniswap pool ID before the _mint OR do not explicitly set the locked value to false when initializing the pool

Assessed type

Under/Overflow

#0 - c4-sponsor

2023-12-17T23:37:37Z

dyedm1 (sponsor) confirmed

#1 - c4-sponsor

2023-12-17T23:37:41Z

dyedm1 marked the issue as disagree with severity

#2 - dyedm1

2023-12-17T23:40:03Z

This is a good one, but might be Med since removedLiquidity is only used for the optional premium accumulators that get exposed and has no effect on the internal SFPM functionality. Panoptic obviously wouldn't execute this reentrancy on its own mints (and besides the factory initializes the pool before deploying a PanopticPool). Essentially it only allows you to screw up your own removedLiquidity, which outside of a protocol is not very relevant.

#3 - c4-judge

2023-12-21T18:42:30Z

Picodes marked the issue as duplicate of #525

#4 - c4-judge

2023-12-21T18:42:52Z

Picodes changed the severity to 2 (Med Risk)

#5 - Picodes

2023-12-21T18:55:51Z

Medium severity seems more appropriate here under "hypothetical attack path with stated assumptions, but external requirement". Storage can be manipulated and this state manipulation could lead to loss of funds in a protocol using the SPFM.

#6 - c4-judge

2023-12-21T18:56:01Z

Picodes marked the issue as satisfactory

#7 - c4-judge

2023-12-21T18:56:09Z

Picodes marked the issue as selected for report

Awards

142.7295 USDC - $142.73

Labels

bug
downgraded by judge
grade-a
QA (Quality Assurance)
satisfactory
sponsor disputed
Q-14

External Links

Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L971-L980 https://github.com/code-423n4/2023-11-panoptic/blob/aa86461c9d6e60ef75ed5a1fe36a748b952c8666/contracts/SemiFungiblePositionManager.sol#L1147-L1153

Vulnerability details

Impact

When someone opens a long position they trigger the SFPM to remove its liquidity from the Uniswap. When the same person wants to close the long position then they trigger the SFPM to replenish the same Uniswap position. To do so, the user has to provide liquidity to the same ticks that he/she removed the liquidity previously. However, since Uniswap pools has such thing called tickSpacingToMaxLiquidityPerTick it might not be possible to close the long position. This is simply that the every tick in Uniswap can hold up to certain amount of liquidity which is called tickSpacingToMaxLiquidityPerTick. If the liquidity to be added makes the existing tick liquidity exceed the max liquidity per tick value, then it will be impossible the mint the liquidity in the given range hence, the long position will not be able to be closed. https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L406-L429 https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Tick.sol#L128

Proof of Concept

Assume a DAI-USDC 0.05% pool. Since this is a stable pool the majority of the liquidity is pretty concentraded in few ticks the liquidity that the specific ticks are extremely liquid. Also, the lesser the tick spacing (fee) the lesser value on the maxLiquidityPerTick because of how Uniswap calculates the max liquidity per tick.

Assume Alice opens a long position by removing from a position in SPFM which corresponds to the ticks x-y. Those ticks x-y has lots of liquidity and they are almost equal to the max liquidity per tick value. After Alice removes the liquidity she will be decrementing the total liquidity in those ticks by the liquidity that she removed. After some time, some people LP's the same ticks and now the Alice can't actually mint the same liquidity because the current liquidity on the tick x or y + the Alice's borrowed liquidity will exceed the max liquidity per tick. Hence, Alice needs to wait for that tick to have available liquidity room to restore her liquidity and close her long position.

This can also be used by an attacker, if the attacker is also short in that same tick range and the current liquidity on x or y ticks are very close to max liquidity per tick, then attacker can top up some liquidity to make the ticks have max liquidity hence effectively preventing Alice to close her long position.

Tools Used

This is a very broad and unlikely issue for volatile pools. However, for stable pools that has lots of liquidity this issue can be realistic. In such cases, preventing this issue fully is probably impossible because SFPM does not controls the Uniswap pool. However, SFPM can control the liquidity provisioning happening through itself. So, if there are some people closing long position and someone opening short position, SFPM can check whether the opening of the short position will make the Uniswap tick value to exceed and reverts.

If there are some liquidity that are borrowed by longers then whenever someone mints (shorts) through SFPM, SFPM can check this invariant: liquidityBorrowedByLongersThroughSFPM + currentLiquidityInThatTickForAllUniswapLpers + requestedShortPositionsMintLiquidity < maxLiquidityPerTick

Assessed type

Other

#0 - c4-sponsor

2023-12-17T23:43:20Z

dyedm1 (sponsor) disputed

#1 - dyedm1

2023-12-17T23:48:04Z

This is great and insightful, but this is kind of a fundamental property of being a V3 liquidity manager. It's impossible to prevent this from happening altogether even if we check that invariant because users can just mint more liquidity in Uniswap instead. Closing a long position is like adding liquidity to Uniswap, so if the max liquidity is exceeded then you can't do it, just like if you were trying to mint a position on another liquidity manager or mint another short position. If users want to get rid of/hedge the other legs in that stuck position if there are any they can simply mint a position with those legs and opposite isLongs and come out net neutral. Ultimately, users have the ability to prevent themselves from having funds stuck in the SFPM by minting long counterparts to short legs, and there's really nothing more you could ask for.

#2 - Picodes

2023-12-26T00:17:48Z

  • I see why it's impossible to prevent this from happening, but also feel it fits C4's definition of Medium severity under "the function of the protocol or its availability could be impacted", so I'll keep this report as a valid medium
  • this being said, from what I can see even for DAI-USDC we are far from the limit and the report doesn't go into details about the likelihood of the scenario

#3 - c4-judge

2023-12-26T00:17:53Z

Picodes marked the issue as satisfactory

#4 - c4-judge

2023-12-26T23:02:18Z

Picodes marked the issue as selected for report

#5 - dyedm1

2023-12-29T15:12:36Z

I still pretty strongly disagree with this issue. The only reason it's here is semantics - unlike other protocols, we represent removed liquidity as positions so preventing the return of that liquidity can be seen as a DoS. This is only true to the extent that the same situation in the NFPM (not being able to add liquidity) is a DoS, given that this is a fundamental property of Uniswap and any other tokens that may be locked in short positions alongside the stuck long liquidity can easily be removed by minting another position.

#6 - Picodes

2024-01-03T11:06:51Z

After consideration, I agree with the previous comment, and considering this is if anything an issue within Uniswap V3 I'll downgrade to Low considering there is no additional issue within the SFPM.

#7 - c4-judge

2024-01-03T11:06:56Z

Picodes changed the severity to QA (Quality Assurance)

#8 - c4-judge

2024-01-05T14:48:25Z

Picodes marked the issue as not selected for report

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter