Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Premium computation is wrong when user go full long on a positionKey #483

Open
c4-bot-4 opened this issue Dec 11, 2023 · 5 comments
Open
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue duplicate-211 grade-b Q-08 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue

Comments

@c4-bot-4
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2023-11-panoptic/blob/f75d07c345fd795f907385868c39bafcd6a56624/contracts/SemiFungiblePositionManager.sol#L1255-L1329

Vulnerability details

Impact

Premium computation is wrong when user want to create a long position. Any protocol that will be created on top of sfpm cannot rely on premium computation of sfpm.

For instance, an user want to short an lp position by first minting an liquidity position on uniswap v3 through sfpm, and then going long (isLong = 1) on the entire liquidity minted previously. In that case, getAccountPremium of sfpm will always return 0 for both tokens. In that case, the premium should be feeGrowthX128 * T * (1+ spread).

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "forge-std/Vm.sol";

import {SemiFungiblePositionManager} from "@contracts/SemiFungiblePositionManager.sol";
import {TokenId} from "@types/TokenId.sol";
import {LeftRight} from "@types/LeftRight.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IUniswapV3Factory} from "v3-core/interfaces/IUniswapV3Factory.sol";
import {IUniswapV3Pool} from "v3-core/interfaces/IUniswapV3Pool.sol";
import {TickMath} from "v3-core/libraries/TickMath.sol";
import {ISwapRouter} from "v3-periphery/interfaces/ISwapRouter.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";

contract PremiumComputationTest is Test, ERC1155Holder {

    using TokenId for uint256;
    using LeftRight for uint256;
    using LeftRight for uint128;
    using LeftRight for int256;

    IERC20 constant public USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IERC20 constant public WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    ISwapRouter constant public SWAPROUTER = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564);
    IUniswapV3Factory constant public FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
    uint24 constant public FEETIER = 500;

    SemiFungiblePositionManager public sfpm;
    IUniswapV3Pool public pool;

    function setUp() public {
        sfpm = new SemiFungiblePositionManager(FACTORY);
        pool = IUniswapV3Pool(FACTORY.getPool(address(USDC), address(WETH), FEETIER));

        // #region initialize pool on sfpm.

        sfpm.initializeAMMPool(address(USDC), address(WETH), FEETIER);

        // #endregion initialize pool on sfpm.
    }

    uint128 public positionSize;
    uint64 public poolId;
    uint160 public sqrtPriceX96;
    int24 public tick;
    int24 public tickSpacing;
    int24 public strike;
    int24 public width;
    uint256 public tokenId;
    uint256 public wethBalance;
    uint128 public premiumToken0;
    uint128 public premiumToken1;

    function test_premium_computation() public {
        // #region input params.

        positionSize = 1e18;

        poolId = sfpm.getPoolId(address(pool));

        // #endregion input params.
        // #region create strike.

        (
            sqrtPriceX96,
            tick,
            ,
            ,
            ,
            ,
            
        ) = pool.slot0();

        tickSpacing = pool.tickSpacing();

        strike = (tick > 0 ? tick / tickSpacing : -tick / tickSpacing) * tickSpacing;

        strike = strike - tickSpacing;

        // #endregion create strike.

        // #region create width.

        width = 2;

        // #endregion create width.

        deal(address(WETH), address(this), 1e18);

        WETH.approve(address(sfpm), 1e18);

        tokenId = uint256(0).addUniv3pool(poolId).addLeg(
            0,
            1,
            1,
            0,
            0,
            0,
            strike,
            width
        );

        sfpm.mintTokenizedPosition(
            tokenId,
            positionSize,
            TickMath.MIN_TICK,
            TickMath.MAX_TICK
        );

        tokenId = uint256(0).addUniv3pool(poolId).addLeg(
            0,
            1,
            1,
            1,
            0,
            0,
            strike,
            width
        );

        sfpm.mintTokenizedPosition(
            tokenId,
            positionSize,
            TickMath.MIN_TICK,
            TickMath.MAX_TICK
        );

        // #region create a swap that make the position cross.

        deal(address(USDC), address(this), 100_000_000 * 1e6);

        USDC.approve(address(SWAPROUTER), 100_000_000 * 1e6);

        SWAPROUTER.exactInputSingle(ISwapRouter.ExactInputSingleParams({
            tokenIn : address(USDC),
            tokenOut : address(WETH),
            fee : 500,
            recipient: address(this),
            deadline: type(uint256).max,
            amountIn : 100_000_000 * 1e6,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: TickMath.getSqrtRatioAtTick(strike - (2 * tickSpacing))
        }));

        deal(address(WETH), address(this), 100_000 * 1e18);

        WETH.approve(address(SWAPROUTER), 100_000 * 1e18);

        SWAPROUTER.exactInputSingle(ISwapRouter.ExactInputSingleParams({
            tokenIn : address(WETH),
            tokenOut : address(USDC),
            fee : 500,
            recipient: address(this),
            deadline: type(uint256).max,
            amountIn : 100_000 * 1e18,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: TickMath.getSqrtRatioAtTick(tick)
        }));

        // #endregion create a swap that make the position cross.

        // #region burn the position.

        {tokenId = uint256(0).addUniv3pool(poolId).addLeg(
            0,
            1,
            1,
            1,
            0,
            0,
            strike,
            width
        );}

        (
            sqrtPriceX96,
            tick,
            ,
            ,
            ,
            ,
            
        ) = pool.slot0();

        (premiumToken0, premiumToken1) = sfpm.getAccountPremium(address(pool), address(this), 0, strike - tickSpacing, strike + tickSpacing, tick, 0);

        console.logUint(premiumToken0);
        console.logUint(premiumToken1);

        (premiumToken0, premiumToken1) = sfpm.getAccountPremium(address(pool), address(this), 0, strike - tickSpacing, strike + tickSpacing, tick, 1);

        console.logUint(premiumToken0);
        console.logUint(premiumToken1);

        deal(address(WETH), address(this), 1e18);

        wethBalance = WETH.balanceOf(address(this));

        WETH.approve(address(sfpm), 1e18);

        sfpm.burnTokenizedPosition(
            tokenId,
            positionSize,
            TickMath.MIN_TICK,
            TickMath.MAX_TICK
        );

        tokenId = uint256(0).addUniv3pool(poolId).addLeg(
            0,
            1,
            1,
            0,
            0,
            0,
            strike,
            width
        );

        sfpm.burnTokenizedPosition(
            tokenId,
            positionSize,
            TickMath.MIN_TICK,
            TickMath.MAX_TICK
        );

        // #region burn the position.

        (
            sqrtPriceX96,
            tick,
            ,
            ,
            ,
            ,
            
        ) = pool.slot0();

        (premiumToken0, premiumToken1) = sfpm.getAccountPremium(address(pool), address(this), 0, strike - tickSpacing, strike + tickSpacing, tick, 0);

        console.logUint(premiumToken0);
        console.logUint(premiumToken1);

        (premiumToken0, premiumToken1) = sfpm.getAccountPremium(address(pool), address(this), 0, strike - tickSpacing, strike + tickSpacing, tick, 1);

        console.logUint(premiumToken0);
        console.logUint(premiumToken1);
    }
}

Tools Used

Recommended Mitigation Steps

Forbid going long on the entire liquidity minted.

Assessed type

Invalid Validation

@c4-bot-4 c4-bot-4 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Dec 11, 2023
c4-bot-2 added a commit that referenced this issue Dec 11, 2023
@dyedm1
Copy link
Member

dyedm1 commented Dec 18, 2023

dup #211,
It's important to note that this account premium calculation is not actually used anywhere within the SFPM, and is an opinionated calculation exposed for optional use by protocols such as Panoptic to price options.

With that being said, part of the opinionated definition of that calculation includes a multiplier on the premium for option buyers based on the utilization on a given chunk. As is illustrated by this graph of the relationship between the utilization and the premium multiplier, there is a vertical asymptote at 100% utilization. Thus, it is expected that protocols using this feature will cap utilization at a certain percentage (for example, Panoptic allows a maximum utilization of 90%).

The long premium calculations at a utilization of 100% (where netLiquidity=0) cannot be performed because the multiplier approaches infinity and would be undefined at that utilization. In that situation, it makes the most sense to forego the premium calculation. Protocols who use this calculation should understand this behavior and either implement a utilization cap or accept that premium will be 0 at utilization=100%

@c4-sponsor
Copy link

dyedm1 (sponsor) disputed

@c4-sponsor c4-sponsor added the sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue label Dec 18, 2023
@c4-judge
Copy link
Contributor

Picodes marked the issue as duplicate of #211

@c4-judge c4-judge added downgraded by judge Judge downgraded the risk level of this issue QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Dec 26, 2023
@c4-judge
Copy link
Contributor

Picodes changed the severity to QA (Quality Assurance)

@c4-judge
Copy link
Contributor

Picodes marked the issue as grade-b

@C4-Staff C4-Staff reopened this Jan 5, 2024
@C4-Staff C4-Staff added the Q-08 label Jan 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working downgraded by judge Judge downgraded the risk level of this issue duplicate-211 grade-b Q-08 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax sponsor disputed Sponsor cannot duplicate the issue, or otherwise disagrees this is an issue
Projects
None yet
Development

No branches or pull requests

5 participants