Panoptic - 0xLogos'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: 1/55

Findings: 2

Award: $28,226.97

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: 0xLogos

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
:robot:_53_group
H-02

Awards

27087.7184 USDC - $27,087.72

External Links

Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L478 https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/CollateralTracker.sol#L461-L467

Vulnerability details

Impact

Malicious actors can mint huge amounts of shares for free and then withdraw all collateral.

Proof of Concept

In the mint function user-controlled shares parameter goes right away to the previewMint function which then calculates required assets in unchecked block. If the shares value is high enough, overflow in shares * DECIMALS will occur, and assets will be very low.

function previewMint(uint shares) public view returns (uint assets) { unchecked { assets = Math.mulDivRoundingUp( shares * DECIMALS, totalAssets(), totalSupply * (DECIMALS - COMMISSION_FEE) ); } } function mint(uint shares, address receiver) external returns (uint assets) { assets = previewMint(shares); if (assets > type(uint104).max) revert Errors.DepositTooLarge(); ... }

Insert the following snippet to ColalteralTracker.t.sol for coded PoC:

function test_poc1(uint256 x) public { _initWorld(x); _grantTokens(Bob); vm.startPrank(Bob); uint shares = type(uint).max / 10000 + 1; IERC20Partial(token0).approve(address(collateralToken0), type(uint256).max); uint256 returnedAssets0 = collateralToken0.mint(shares, Bob); assertEq(shares, collateralToken0.balanceOf(Bob)); assertEq(returnedAssets0, 1); }

Tools Used

Manual review

Remove unchecked block

function maxMint(address) external view returns (uint maxShares) { return (convertToShares(type(uint104).max) * DECIMALS) / (DECIMALS + COMMISSION_FEE); }

Assessed type

Under/Overflow

#0 - c4-judge

2024-04-24T17:56:31Z

Picodes marked the issue as primary issue

#1 - c4-judge

2024-05-06T14:54:00Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2024-05-06T16:03:03Z

Picodes marked the issue as selected for report

Findings Information

🌟 Selected for report: pkqs90

Also found by: 0xLogos, Udsen, bin2chen

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
:robot:_178_group
duplicate-498

Awards

1139.2469 USDC - $1,139.25

External Links

Lines of code

https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/libraries/PanopticMath.sol#L100-L109 https://github.com/code-423n4/2024-04-panoptic/blob/833312ebd600665b577fbd9c03ffa0daf250ed24/contracts/PanopticPool.sol#L1367

Vulnerability details

Impact

A malicious actor can forge the currently held positions list and trick the system into believing that, for example, their overall position is solvent.

Proof of Concept

The user's currently held positions list is not stored on-chain, instead, it is provided with each mintOptions/burnOptions call, hashed, and then compared to the stored hash (fingerprint) in _validatePositionList.

function _validatePositionList( address account, TokenId[] calldata positionIdList, uint256 offset ) internal view { uint256 pLength; uint256 currentHash = s_positionsHash[account]; unchecked { pLength = positionIdList.length - offset; } uint256 fingerprintIncomingList; for (uint256 i = 0; i < pLength; ) { fingerprintIncomingList = PanopticMath.updatePositionsHash( fingerprintIncomingList, positionIdList[i], ADD ); unchecked { ++i; } } if (fingerprintIncomingList != currentHash) revert Errors.InputListFail(); }

updatePositionsHash XORes keccak of each token in right 248 bits and stores positions count in left 8 bit.

function updatePositionsHash( uint256 existingHash, TokenId tokenId, bool addFlag ) internal pure returns (uint256) { unchecked { uint248 updatedHash = uint248(existingHash) ^ (uint248(uint256(keccak256(abi.encode(tokenId))))); return addFlag ? uint256(updatedHash) + (((existingHash >> 248) + 1) << 248) : uint256(updatedHash) + (((existingHash >> 248) - 1) << 248); } }

This validation can be bypassed when addFlag=true by overflowing the counter and hiding tokens by doubling (x ^ x = 0, x ^ 0 = x). For example, assuming counter has only 2 bits: [x, x, x, x, a] => x ^ x ^ x ^ x ^ a | 1 + 1 + 1 + 1 + 1 => a | 1.

For coded PoC make the following changes in, for example, test_Success_mintOptions_OTMShortCall:

- TokenId[] memory posIdList = new TokenId[](1); - posIdList[0] = tokenId; + TokenId[] memory posIdList = new TokenId[](257); + for (uint i; i < 257; i++) posIdList[i] = tokenId;

Validating position list will pass meaning we successfully "hide" 256 tokens from the validator, but they still counted in _validateSolvency and other functions.

Tools Used

Manual review

Enforce MAX_POSITIONS limit in _validatePositionList by adding require: require(positionIdList.length <= MAX_POSITIONS)

Assessed type

Invalid Validation

#0 - c4-judge

2024-04-25T08:41:19Z

Picodes marked the issue as duplicate of #498

#1 - c4-judge

2024-05-06T16:06:03Z

Picodes marked the issue as satisfactory

#2 - c4-judge

2024-05-09T19:28:13Z

Picodes changed the severity to 2 (Med Risk)

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