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: 38/55
Findings: 1
Award: $32.96
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: DadeKuma
Also found by: 0xStalin, 0xhacksmithh, 99Crits, Aymen0909, Bauchibred, CodeWasp, Dup1337, IllIllI, John_Femi, K42, KupiaSec, Naresh, Rhaydden, Rolezn, Sathish9098, Topmark, ZanyBonzy, albahaca, bareli, blockchainbuttonmasher, cheatc0d3, codeslide, crc32, d3e4, favelanky, grearlake, hihen, jasonxiale, jesjupyter, lanrebayode77, lirezArAzAvi, lsaudit, mining_mario, oualidpro, pfapostol, radin100, rbserver, sammy, satoshispeedrunner, slvDev, twcctop, zabihullahazadzoi
32.9585 USDC - $32.96
CollateralTracker
contractCollateralTracker
is a token vault contract.
It possible that some token could remain in that contract at the, due to some precision loss or rounding down/up opration as Dusts
So developer should implemnt some token extract method so that those Dust amount could pulled out of collateralTracker contracts.
Or in most hazardious situation let say in some attack case, it could stop attacker from daring fund.
Protocol should implement some sort of pause / upgradable
methods. I mention those in Non-critical section.
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol
maxMint
functionIn Solidity division can result in rounding down errors, hence to minimize any rounding errors we always want to perform multiplication before division.
function maxMint(address) external view returns (uint256 maxShares) { unchecked { return (convertToShares(type(uint104).max) * DECIMALS) / (DECIMALS + COMMISSION_FEE); } } function convertToShares(uint256 assets) public view returns (uint256 shares) { return Math.mulDiv(assets, totalSupply, totalAssets()); }
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L444-L448
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L379-L381
Net, Gross, and Owed fees (with spread)
sectiongross_feesCollectedX128 = feesGrowthX128 * N + feesGrowthX128*ν*S*(1 + S/N)
--Eq-1
According to Docs we get following equation(Eq-2) by solving above(Eq-1)
gross_feesCollectedX128 = feesGrowthX128 * T * (1 + ν*S^2/(N*T))
-- Eq-2
which is not true
To get Eq-2 from above we have to assume that T = N + VS
which is not right
Actually T = N + S
That shows that above equation(Eq-1) was wrong
https://panoptic.xyz/docs/panoptic-protocol/streamia#net-gross-and-owed-fees-with-spread
maxDeposit
is incompatible with some ERC20 tokensmaxDeposit() returns that maximum amount asset that can be deposited to collateralTracker
contract. it returns type(uint104).max
But problem is that some tokens like COMP
UNI
ony support maxTransfer or maxAllowance till type(uint96).max
.
https://www.youtube.com/watch?v=Tx0f8A2Yd3k&t=2090s
So for those type of Assets here maxDeposit()
retuning wrong values.
/// @notice returns The maximum deposit amount. /// @return maxAssets The maximum amount of assets that can be deposited. function maxDeposit(address) external pure returns (uint256 maxAssets) { return type(uint104).max; }
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L392-L394
maxMint
calculation is wrongIn ERC4626 standard's word maxMint() MUST return the maximum amount of shares mint would allow to be deposited to receiver and not cause a revert,
function maxMint(address) external view returns (uint256 maxShares) { unchecked { return (convertToShares(type(uint104).max) * DECIMALS) / (DECIMALS + COMMISSION_FEE); } } function convertToShares(uint256 assets) public view returns (uint256 shares) { return Math.mulDiv(assets, totalSupply, totalAssets());
But in above implementation we see so many rounding downs
convertToShares
maxShares
calculationAs a result maxMint()
not returning maximum possible shares
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L444-L448
constructor( address _WETH9, SemiFungiblePositionManager _SFPM, IUniswapV3Factory _univ3Factory, IDonorNFT _donorNFT, address _poolReference, address _collateralReference ) { WETH = _WETH9; // @audit L :: address check in constructor SFPM = _SFPM; DONOR_NFT = _donorNFT; // We store the Uniswap Factory contract - later we can use this to verify uniswap pools UNIV3_FACTORY = _univ3Factory; POOL_REFERENCE = _poolReference; COLLATERAL_REFERENCE = _collateralReference; }
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticFactory.sol#L115-L130
deposit()
doesn't fully followed ERC4626
standardaccording to EIP-4626 standard following s the required condition for deposit()
MUST support EIP-20 approve / transferFrom on asset as a deposit flow. MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the deposit execution, and are accounted for during deposit.
This EIP-20 approve support feature already implemented in case of CallateralTracker.sol's withdraw() & redeem(). So its recommened to follow same feature in case of deposit as well.
function deposit(uint256 assets, address receiver) external returns (uint256 shares) { // @audit-issue incompatible with erc4626 as EIP-20 approve if (assets > type(uint104).max) revert Errors.DepositTooLarge(); shares = previewDeposit(assets); SafeTransferLib.safeTransferFrom( s_underlyingToken, msg.sender, address(s_panopticPool), assets ); _mint(receiver, shares); // update tracked asset balance unchecked { s_poolAssets += uint128(assets); } emit Deposit(msg.sender, receiver, assets, shares); }
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L417-L440
Contracts should avoid making low-level calls to custom addresses, especially if these calls are based on address parameters in the function. Such behavior can lead to unexpected execution of untrusted code. Instead, consider using Solidity's high-level function calls or contract interactions.
return ERC20Minimal.transfer(recipient, amount); return ERC20Minimal.transferFrom(from, to, amount);
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L333 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L352
Emitting an initialization event offers clear, on-chain evidence of the contract's initialization state, enhancing transparency and auditability. This practice aids users and developers in accurately tracking the contract's lifecycle, pinpointing the precise moment of its initialization. Moreover, it aligns with best practices for event logging in smart contracts, ensuring that significant state changes are both observable and verifiable through emitted events.
totalSupply = 10 ** 6; s_poolAssets = 1; s_underlyingToken = underlyingIsToken0 ? token0 : token1; s_panopticPool = panopticPool; COMMISSION_FEE = _commissionFee; SELLER_COLLATERAL_RATIO = _sellerCollateralRatio; BUYER_COLLATERAL_RATIO = _buyerCollateralRatio; FORCE_EXERCISE_COST = _forceExerciseCost; TARGET_POOL_UTIL = _targetPoolUtilization; SATURATED_POOL_UTIL = _saturatedPoolUtilization; ITM_SPREAD_MULTIPLIER = _ITMSpreadMultiplier;
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L187-L193 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L238 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L234 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L244
Combining multiple address/ID mappings into a single mapping to a struct can enhance code clarity and maintainability. Consider refactoring multiple mappings into a single mapping with a struct for cleaner code structure. This arrangement also promotes a more organized contract structure, making it easier for developers to navigate and understand.
Instances(2)
mapping(address account => mapping(TokenId tokenId => mapping(uint256 leg => LeftRightUnsigned premiaGrowth))) internal s_options; mapping(address account => mapping(TokenId tokenId => LeftRightUnsigned balanceAndUtilizations)) internal s_positionBalance;
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L238
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L258
While it does not save gas for some simple binary expressions because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand. There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.
Instances(3)
int24 internal constant MIN_SWAP_TICK = Constants.MIN_V3POOL_TICK + 1; int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK - 1; uint64 internal constant MAX_SPREAD = 9 * (2 ** 32);
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L103 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L105 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L165
Contract uses a non-upgradeable design. Transitioning to an upgradeable contract structure is more aligned with contemporary smart contract practices. This approach not only enhances flexibility but also allows for continuous improvement and adaptation, ensuring the contract stays relevant and robust in an ever-evolving ecosystem.
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol
Smart contracts that hold significant value, interact with external contracts, or have complex logic should include an emergency-stop mechanism for added security. This allows pausing certain contract functionalities in case of emergencies, mitigating potential damages.
This contract seems to lack such a mechanism. Implementing an emergency stop can enhance contract security and reliability. https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol
While adding a block or deny-list may increase the level of centralization in the contract, it provides an additional layer of security by preventing hackers from using stolen tokens or carrying out other malicious activities.
Although it's a trade-off, a block or deny-list can help improve the overall security posture of the contract.
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol
The functions in question operate on arrays without established boundaries, executing function calls for each of their entries. If these arrays become excessively long, a function might revert due to gas constraints. To enhance user experience, consider incorporating a require() statement that enforces a sensible maximum array length. This approach can avoid unnecessary computational work and ensure smoother transactions.
Instances()
uint256 pLength = positionIdList.length; balances = new uint256[2][](pLength); for (uint256 k = 0; k < pLength; ) { for (uint256 i = 0; i < positionIdList.length; ) { for (uint256 i = 0; i < pLength; ) {
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L441 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L802 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L1382
Complex casting in Solidity contracts can introduce both readability issues and potential for overflows. Whenever multiple casts are found, consider whether the complexity is necessary. If so, it is strongly recommended to add inline comments to explain why these casts are needed and to assure that no overflows are introduced.
Instances(Multiple instances in following contracts)
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/types/LeftRight.sol https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/types/LiquidityChunk.sol
Placing constants on the left side of comparison statements can help prevent accidental assignments and improve code readability. In languages like C, placing constants on the left can protect against unintended assignments that would be treated as true conditions, leading to bugs. Although Solidity does not have this specific issue, using the same practice can still be beneficial for code readability and consistency.
Consider placing constants on the left side of comparison operators like ==, !=, <, >, <=, and >=."
Instances()
if (currentSqrtPriceX96 <= sqrtLowerBound || currentSqrtPriceX96 >= sqrtUpperBound) { if (Math.abs(int256(fastOracleTick) - slowOracleTick) > MAX_SLOW_FAST_DELTA) if (Math.abs(currentTick - twapTick) > MAX_TWAP_DELTA_LIQUIDATION)
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L341 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L943 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L1035
The use of the delete keyword is recommended over simply assigning values to false when you intend to reset the state of a variable. The delete keyword more closely aligns with the semantic intent of clearing or resetting a variable. This practice also makes the code more readable and highlights the change in state, which may encourage a more thorough audit of the surrounding logic.
Instances()
for (uint256 leg = 0; leg < numLegs; ) { ... ... s_options[owner][tokenId][leg] = LeftRightUnsigned.wrap(0); ... }
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L870
Instances(4)
bool internal constant COMPUTE_LONG_PREMIA = false; bool internal constant ONLY_AVAILABLE_PREMIUM = false; bool internal constant DONOT_COMMIT_LONG_SETTLED = false; bool internal constant SLOW_ORACLE_UNISWAP_MODE = false;
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L111 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L114 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L120 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L133
Using named returns makes the code more self-documenting, makes it easier to fill out NatSpec, and in some cases can save gas. The cases below are where there currently is at most one return statement, which is ideal for named returns.
Instances()
function _getSlippageLimits( int24 tickLimitLow, int24 tickLimitHigh ) internal pure returns (int24, int24) { function _mintInSFPMAndUpdateCollateral( TokenId tokenId, uint128 positionSize, int24 tickLimitLow, int24 tickLimitHigh ) internal returns (uint128) { function _payCommissionAndWriteData( TokenId tokenId, uint128 positionSize, LeftRightSigned totalSwapped ) internal returns (uint128) {
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L502 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L682 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L710
The recent updates in Solidity provide several features and optimizations that, when leveraged appropriately, can significantly improve your contract's code clarity and maintainability. Key enhancements include the use of push0 for placing 0 on the stack for EVM versions starting from "Shanghai", making your code simpler and more straightforward. Moreover, Solidity has extended NatSpec documentation support to enum and struct definitions, facilitating more comprehensive and insightful code documentation.
Additionally, the re-implementation of the UnusedAssignEliminator and UnusedStoreEliminator in the Solidity optimizer provides the ability to remove unused assignments in deeply nested loops. This results in a cleaner, more efficient contract code, reducing clutter and potential points of confusion during code review or debugging. It's recommended to make full use of these features and optimizations to enhance the robustness and readability of your smart contracts.
TICK_DEVIATION = uint256( 2230 + (12500 * ratioTick) / 10_000 + (7812 * ratioTick ** 2) / 10_000 ** 2 + (6510 * ratioTick ** 3) / 10_000 ** 3 );
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/CollateralTracker.sol#L200-L209
constant
should be value rather than expressionInstances(3)
int24 internal constant MIN_SWAP_TICK = Constants.MIN_V3POOL_TICK + 1; int24 internal constant MAX_SWAP_TICK = Constants.MAX_V3POOL_TICK - 1; uint64 internal constant MAX_SPREAD = 9 * (2 ** 32);
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L103 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L105 https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L165
s_miniMedian = (uint256(block.timestamp) << 216) + (uint256(0xF590A6F276170D89E9F276170D89E9F276170D89E9000000000000)) +
https://github.com/code-423n4/2024-04-panoptic/blob/main/contracts/PanopticPool.sol#L312
#0 - c4-judge
2024-04-26T17:20:27Z
Picodes marked the issue as grade-b