Panoptic - KupiaSec's results

Permissionless, perpetual options trading on any token, any strike, any size.

General Information

Platform: Code4rena

Start Date: 01/04/2024

Pot Size: $120,000 USDC

Total HM: 11

Participants: 55

Period: 21 days

Judge: Picodes

Total Solo HM: 6

Id: 354

League: ETH

Panoptic

Findings Distribution

Researcher Performance

Rank: 4/55

Findings: 2

Award: $8,159.28

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: KupiaSec

Labels

bug
2 (Med Risk)
satisfactory
selected for report
sponsor confirmed
:robot:_479_group
M-04

Awards

8126.3155 USDC - $8,126.32

External Links

Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticPool.sol#L1483

Vulnerability details

Impact

Because of incorrect validation, it allows option buyers not to pay premium.

Proof of Concept

When long leg is minted or short leg is burnt, the protocol checks liquidity spread by calculating TotalLiquidity / NetLiquidity and allows it not exceed 9. However in the check function, the validation is ignored when NetLiquidity is zero.

This means when a user mints long leg that buys whole selling amount, the liquidity spread is not checked. This issue allows the option buyer not to pay premium, and here is why:

  1. When options are minted, last accumulated premium is stored to s_grossPremiumLast. Refer to _updateSettlementPostMint function of PanopticPool contract.
  2. When options are burned, new accumulated premium is fetched and calculates the premium by multiplying liquidity with difference in accumulated premium. Refer to _updateSettlementPostBurn function of PanopticPool contract.
  3. However, when a long leg of T(total liquidity) amount is minted, N becomes zero.
  4. Later, when the minted long leg is burnt, premium values are not updated in SFPM, because N is zero. Refer to SFPM:L1085

Since there is no difference in owed premium value, the option buyer will not pay the premium when burning the option.

Tools Used

Manual Review

When checking liquidity spread, it should revert when N is zero and T is positive:

+   if(netLiquidity == 0 && totalLiquidity > 0) revert;
    if(netLiquidity == 0) return;

Assessed type

Context

#0 - c4-judge

2024-05-06T14:53:07Z

Picodes marked the issue as satisfactory

#1 - c4-judge

2024-05-06T14:53:10Z

Picodes marked the issue as selected for report

Awards

32.9585 USDC - $32.96

Labels

bug
downgraded by judge
grade-b
primary issue
QA (Quality Assurance)
sponsor disputed
:robot:_312_group
Q-12

External Links

Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/SemiFungiblePositionManager.sol#L999-L1035 https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticPool.sol#L836-L847

Vulnerability details

Impact

Option sellers won't be able to close short positions because of existing dust amount of buy, thus might cause a few issues:

  1. Notional amount of tokens can't be withdrawn back to the pool.
  2. Option sellers are not be able to withdraw their deposits from the pool.

Proof of Concept

PanopticPool contract only allows option sellers to burn entire position size of an option. By using feature/vulnerability, malicious users can buy dust amount of options to prevent option sellers from closing their positions.

Here's a PoC written in Foundry that shows an option seller not being able to close position:

function test_Audit_dosBurn(
    uint256 x,
    uint256 widthSeed,
    int256 strikeSeed,
    uint256 positionSizeSeed
) public {
    // Prepare pool
        _initPool(x);
    (int24 width, int24 strike) = PositionUtils.getOTMSW(
        widthSeed,
        strikeSeed,
        uint24(tickSpacing),
        currentTick,
        0
    );
    populatePositionData(width, strike, positionSizeSeed);

    // Create a short position
    TokenId shortTokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, isWETH, 0, 0, 0, strike, width);
    TokenId[] memory posIdList = new TokenId[](1);
    posIdList[0] = shortTokenId;
    // Bob sells positionSize amount of options
    vm.startPrank(Bob);
    pp.mintOptions(posIdList, positionSize, 0, 0, 0);

    // Create a buy position
    TokenId longTokenId = TokenId.wrap(0).addPoolId(poolId).addLeg(0, 1, isWETH, 1, 0, 0, strike, width);
    posIdList[0] = longTokenId;
    // Malicious Alice buys dust amount of options, e.g 1e4
    vm.startPrank(Alice);
    pp.mintOptions(posIdList, 1e4, type(uint64).max - 1, 0, 0);

    // Later, Bob tries to burn the position, but not able to burn it
    vm.startPrank(Bob);
    vm.expectRevert();
    pp.burnOptions(shortTokenId, new TokenId[](0), 0, 0);
}

Tools Used

Manual Review, Foundry

Option sellers should be able to close partial amount of their positions. Or, only allow to buy meaningful minimum amount of options.

Assessed type

DoS

#0 - c4-judge

2024-04-26T18:36:11Z

Picodes marked the issue as primary issue

#1 - dyedm1

2024-04-26T20:54:33Z

Options sellers can force exercise buyers if they are out-of-range, and they can also sell a smaller position in the same chunk if they want to close their position and it is truly a "dust" amount purchased.

#2 - c4-judge

2024-05-06T15:36:33Z

Picodes changed the severity to QA (Quality Assurance)

#3 - Picodes

2024-05-06T15:36:50Z

Downgrading to Low following the sponsor's answer

#4 - c4-judge

2024-05-06T16:07:25Z

Picodes marked the issue as grade-c

#5 - c4-judge

2024-05-06T16:07:28Z

Picodes marked the issue as grade-b

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