Platform: Code4rena
Start Date: 03/10/2023
Pot Size: $24,500 USDC
Total HM: 6
Participants: 62
Period: 3 days
Judge: LSDan
Total Solo HM: 3
Id: 288
League: ETH
Rank: 23/62
Findings: 2
Award: $87.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: adriro
Also found by: 0x3b, 0xAadi, 0xDING99YA, 0xTheC0der, 0xWaitress, 0xdice91, 100su, 3docSec, BRONZEDISC, BoRonGod, Eurovickk, GKBG, HChang26, IceBear, JP_Courses, MatricksDeCoder, Mike_Bello90, SovaSlava, Topmark, albahaca, cookedcookee, gzeon, hunter_w3b, kutugu, lukejohn, marqymarq10, matrix_0wl, orion, pep7siup, radev_sw, sces60107, taner2344, tpiliposian, wahedtalash77, xAriextz, zpan
78.3912 USDC - $78.39
The above has examples of arrays taking in values e.g. weeks to claim for concentrated liquidity. There are no checks that any of the values in the array is != 0. Its possible front ends or user or entry errors can lead to zero values(default for uint) passed in by error.
The above can lead to unexpected behaviours, potential errors etc. Consider
//canto_ambient/contracts/mixins/LiquidityMining.sol //uint32 week = weeksToClaim[i]; week not checked to not be 0 value for (uint256 i; i < weeksToClaim.length; ++i) { uint32 week = weeksToClaim[i]; require(week + WEEK < block.timestamp, "Week not over yet"); require( !concLiquidityRewardsClaimed_[poolIdx][posKey][week], "Already claimed" );
https://github.com/code-423n4/2023-10-canto/blob/29c92a926453a49c8935025a4d3de449150fc2ff/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L65 uint64 weeklyReward must not be 0
https://github.com/code-423n4/2023-10-canto/blob/29c92a926453a49c8935025a4d3de449150fc2ff/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L74 uint64 weeklyReward must not be 0
function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; // wrong rewards stored weekFrom += uint32(WEEK); } }
weeklyReward may be passed in accidentally, by error, or by default value as 0 leading to the storage of pools weekFrom rewards incorrectly stored as 0 leading to loss of rewards if not picked up
Recommendation -> It is recommended to ensure all cases where inputs of values = 0 in functions are checked and ensure there is revert in such cases so that zero values are not allowed e.g
require(weeklyReward != 0, "cant be zero")
There is no check that inputs weekFrom <= weeTo setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward)
function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } } function setAmbRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } }
If values weekFrom and weekTo are switched by error it can lead to AmbRewards and or ConRewards not being set with a thinking that they have been set as the while loop will not run https://github.com/code-423n4/2023-10-canto/blob/29c92a926453a49c8935025a4d3de449150fc2ff/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L68 https://github.com/code-423n4/2023-10-canto/blob/29c92a926453a49c8935025a4d3de449150fc2ff/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L77
uint256 constant WEEK = 604800; // Week in seconds 604800
There exists in solidity time units for days, weeks, hours, years, etc Recommended to use solidity integrated time units to increase code readability and avoid calculation error.
It is considered best practise to underscore function arguments/parameters for readablity, ensure not clash or confuse with state variables etc
All the functions in the code do not make use of this pre or post underscore format
Recommendation -> It is recommended to underscore to functions parameters/arguments e.g as below
function setConcRewards(bytes32 _poolIdx, uint32 _weekFrom, uint32 _weekTo, uint64 _weeklyReward) public payable {
It is considered best practise to underscore internal and private functions e.g
function _crossTicks(bytes32 poolIdx,int24 exitTick, int24 entryTick) internal {
All the functions in /canto_ambient/contracts/mixins/LiquidityMining.sol are internal but do not make use of this best practise https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L161
The contracts use version 0.8.19 when latest version is 0.8.21. Latest version may have benefited from bug fixes, optimizations, patches, improvements, that still are in 0.8.19 and may put project at risk.
Functions especially those used by users to interact with the protocol/project need to have intuitive names that can readily help users/stakeholders have an idea what the function is doing. The following functions are examples of non intuitive naming e.g few examples below
userCmd -> userLiquidityCommands accrueConcentratedPositionTimeWeightedLiquidity // is too long a name
Consider review all relevant functions and rename appropriately considering meaning, length, appropriateness etc
/* @title Liquidity mining callpath sidecar. * @notice Defines a proxy sidecar contract that's used to move code outside the * main contract to avoid Ethereum's contract code size limit. Contains * components related to CANTO liquidity mining. * * @dev This exists as a standalone contract but will only ever contain proxy code, * not state. As such it should never be called directly or externally, and should * only be invoked with DELEGATECALL so that it operates on the contract state * within the primary CrocSwap contract. * @dev Since this contract is a proxy sidecar, entrypoints need to be marked * payable even though it doesn't directly handle msg.value. Otherwise it will * fail on any. Because of this, this contract should never be used in any other * context besides a proxy sidecar to CrocSwapDex. */
The spacing of last @dev comments does not correspond with earlier @dev and @notice
The following functions share common code canto_ambient/contracts/callpaths/LiquidityMiningPath.sol
function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } } function setAmbRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } }
We can see the same update for weeks and same require in functions. An example improvement is below
function setRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward, bool Amb) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { if(Amb) {ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward;} else {concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward;} weekFrom += uint32(WEEK); } }
Additional functions sharing coming code are
function accrueConcentratedGlobalTimeWeightedLiquidity( function accrueAmbientGlobalTimeWeightedLiquidity( \\ in the parts below that are almost similar if (lastAccrued != 0) { uint256 liquidity = curve.ambientSeeds_; uint32 time = lastAccrued; while (time < block.timestamp) { uint32 currWeek = uint32((time / WEEK) * WEEK); uint32 nextWeek = uint32(((time + WEEK) / WEEK) * WEEK); uint32 dt = uint32( nextWeek < block.timestamp ? nextWeek - time : block.timestamp - time ); timeWeightedWeeklyGlobalAmbLiquidity_[poolIdx][currWeek] += dt * liquidity; time += dt; } } timeWeightedWeeklyGlobalAmbLiquidityLastSet_[poolIdx] = uint32( block.timestamp );
Consider the appropriate refactors that work best in order to avoid code duplication, reuse, bloat, repetition etc
#0 - c4-pre-sort
2023-10-09T17:23:09Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-10-18T23:11:41Z
dmvt marked the issue as grade-a
🌟 Selected for report: niser93
Also found by: 0xAnah, JCK, MatricksDeCoder, Polaris_tow, Raihan, SAQ, SY_S, debo, hihen, hunter_w3b, lsaudit, naman1778, pipidu83, shamsulhaq123, tabriz
8.6695 USDC - $8.67
There are several instances in same function where mappings are reread/reaccessed see below in
function crossTicks( bytes32 poolIdx, int24 exitTick, int24 entryTick ) internal { uint256 numElementsExit = tickTracking_[poolIdx][exitTick].length; //1 tickTracking_[poolIdx][exitTick][numElementsExit - 1] //2 .exitTimestamp = uint32(block.timestamp); StorageLayout.TickTracking memory tickTrackingData = StorageLayout .TickTracking(uint32(block.timestamp), 0); tickTracking_[poolIdx][entryTick].push(tickTrackingData); //3 }
tickTracking_[poolIdx] or tickTracking_[poolIdx][exitTick] can be saved in a storage pointer
function accrueConcentratedGlobalTimeWeightedLiquidity( bytes32 poolIdx, CurveMath.CurveState memory curve ) internal { uint32 lastAccrued = timeWeightedWeeklyGlobalConcLiquidityLastSet_[ poolIdx ]; //1 // Only set time on first call if (lastAccrued != 0) { uint256 liquidity = curve.concLiq_; uint32 time = lastAccrued; while (time < block.timestamp) { uint32 currWeek = uint32((time / WEEK) * WEEK); uint32 nextWeek = uint32(((time + WEEK) / WEEK) * WEEK); uint32 dt = uint32( nextWeek < block.timestamp ? nextWeek - time : block.timestamp - time ); timeWeightedWeeklyGlobalConcLiquidity_[poolIdx][currWeek] += dt * liquidity; time += dt; } } timeWeightedWeeklyGlobalConcLiquidityLastSet_[poolIdx] = uint32( block.timestamp ); //2 }
timeWeightedWeeklyGlobalConcLiquidityLastSet_[poolIdx] can be saved in a storage pointer
function accrueConcentratedPositionTimeWeightedLiquidity( .... uint32 lastAccrued = timeWeightedWeeklyPositionConcLiquidityLastSet_[ poolIdx ][posKey]; //1 line 82 ...... uint32 tickTrackingIndex = tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i]; // 1 line 89 .... tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = tickTrackingIndex; // 2 line 135 .... tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = numTickTracking - 1; // 3 line 144 .... tickTrackingIndexAccruedUpTo_[poolIdx][posKey][i] = numTickTracking; //4 line 146 timeWeightedWeeklyPositionConcLiquidityLastSet_[poolIdx][ posKey ] = uint32(block.timestamp); //2 line 151 }
timeWeightedWeeklyPositionConcLiquidityLastSet_[poolIdx][posKey] can have a storage pointer created tickTrackingIndexAccruedUpTo_[poolIdx][posKey] used many times can be saved in storage pointer
The above are just examples but functions in LiquidityMining.sol e.g timeWeightedWeeklyPositionAmbLiquidityLastSet_[ poolIdx ][posKey] in function accrueAmbientGlobalTimeWeightedLiquidity; and may other instances in which mappings are not cached or saved in storage pointer in cases where they are used several times in other remaining functions.
Impact -> Caching a mapping's value in a storage pointer when the value is accessed multiple times saves ~40 gas per access due to not having to perform the same offset calculation every time.
Recommendation -> Check all functions and Save a storage pointer for the mapping variable and use it instead of repeatedly fetching the reference in a map
There are functions with duplicated code parts that can have those parts placed in separate functions or have the functions combined into a single function.
he following functions share common code canto_ambient/contracts/callpaths/LiquidityMiningPath.sol
function setConcRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } } function setAmbRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; weekFrom += uint32(WEEK); } }
We can see the same update for weeks and same require in functions. An example improvement is below
function setRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward, bool Amb) public payable { // require(msg.sender == governance_, "Only callable by governance"); require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { if(Amb) {ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward;} else {concRewardPerWeek_[poolIdx][weekFrom] = weeklyReward;} weekFrom += uint32(WEEK); } }
Additional functions sharing coming code are
function accrueConcentratedGlobalTimeWeightedLiquidity( function accrueAmbientGlobalTimeWeightedLiquidity( \\ in the parts below that are almost similar if (lastAccrued != 0) { uint256 liquidity = curve.ambientSeeds_; uint32 time = lastAccrued; while (time < block.timestamp) { uint32 currWeek = uint32((time / WEEK) * WEEK); uint32 nextWeek = uint32(((time + WEEK) / WEEK) * WEEK); uint32 dt = uint32( nextWeek < block.timestamp ? nextWeek - time : block.timestamp - time ); timeWeightedWeeklyGlobalAmbLiquidity_[poolIdx][currWeek] += dt * liquidity; time += dt; } } timeWeightedWeeklyGlobalAmbLiquidityLastSet_[poolIdx] = uint32( block.timestamp );
Consider the appropriate refactors that work best in order to avoid code duplication, reuse, bloat, repetition etc
#0 - c4-pre-sort
2023-10-09T17:18:03Z
141345 marked the issue as sufficient quality report
#1 - c4-judge
2023-10-18T23:16:07Z
dmvt marked the issue as grade-b