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: 62/62
Findings: 1
Award: $4.94
π 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
4.9369 USDC - $4.94
https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L256-L290 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L256-L271 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L277-L280 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/callpaths/LiquidityMiningPath.sol#L74-L79 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L277-L280 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L283 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L269-L271 https://github.com/code-423n4/2023-10-canto/blob/main/canto_ambient/contracts/mixins/LiquidityMining.sol#L156-L196
In the event in which a LP has been providing liquidity for a week , the AmbRewards
and ConcRewards
were not set and LP claims his rewards he won't have anything to claim. The impact of this issue is high because it can lead to loss of funds for a LP , however the likelihood is low , hence the overall severity is medium.
First we have the LiquidityMining.claimAmbientRewards
function :
function claimAmbientRewards(address owner,bytes32 poolIdx,uint32[] memory weeksToClaim) internal
In the beginning of the function the pool
is provided , the liquidity
for the particular position and the global AmbientGlobal
liquidity
are accrued for. Iterating through the weeksToClaim
array we make sure that the whole week
is finished . The rewards are checked if they are not already claimed.
function claimAmbientRewards( address owner, bytes32 poolIdx, uint32[] memory weeksToClaim ) internal { CurveMath.CurveState memory curve = curves_[poolIdx]; accrueAmbientPositionTimeWeightedLiquidity(payable(owner), poolIdx); accrueAmbientGlobalTimeWeightedLiquidity(poolIdx, curve); bytes32 posKey = encodePosKey(owner, poolIdx); uint256 rewardsToSend; for (uint256 i; i < weeksToClaim.length; ++i) { uint32 week = weeksToClaim[i]; require(week + WEEK < block.timestamp, "Week not over yet"); require( !ambLiquidityRewardsClaimed_[poolIdx][posKey][week], "Already claimed" );
Then rewardsToSend
are calculated. This calculation happens on lines L277-L280
uint256 rewardsForWeek = (timeWeightedWeeklyPositionAmbLiquidity_[ poolIdx ][posKey][week] * ambRewardPerWeek_[poolIdx][week]) / overallTimeWeightedLiquidity; rewardsToSend += rewardsForWeek;
The multiplicator in this equation is ambRewardPerWeek_[poolIdx][week]
. This value is set by governance in LiquidityMiningPath.sol
in the function setAmbRewards()
(L74-L79).
function setAmbRewards(bytes32 poolIdx, uint32 weekFrom, uint32 weekTo, uint64 weeklyReward) public payable { // require(msg.sender == governance_, "Only callable by governance"); @audit // commented out code require(weekFrom % WEEK == 0 && weekTo % WEEK == 0, "Invalid weeks"); while (weekFrom <= weekTo) { ambRewardPerWeek_[poolIdx][weekFrom] = weeklyReward; //(pool => week => reward) weekFrom += uint32(WEEK); } }
The ambRewardPerWeek_
mapping is of type pool => week => reward
. In the event in which rewards are not set yet and a LP has provided liquidity
already for at least a week , the ambRewardPerWeek_
are 0. Then the equation on lines L277-L280 of LiquidityMining.claimAmbientRewards()
becomes equal to :
uint256 rewardsForWeek = (timeWeightedWeeklyPositionAmbLiquidity_[ poolIdx ][posKey][week] * 0 ) / overallTimeWeightedLiquidity; rewardsToSend += rewardsForWeek;
rewardsToSend
become 0. The issue lies here. On L283 the ambLiquidityRewardsClaimed_
for the particular LP
and week
is set to true
} ambLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; }
After that there is an if
statement which checks rewardsToSend
to be greather than 0 and only then rewards are sent
if (rewardsToSend > 0) { (bool sent, ) = owner.call{value: rewardsToSend}(""); require(sent, "Sending rewards failed"); } } }
In our case the rewardsToSend
are 0 and this if
won't be entered but the ambLiquidityRewardsClaimed_[poolIdx][posKey][week] = true;
is set to true. So rewards
for that week
are "claimed"
although AmbRewards
were not set. LP
looses the chance to earn rewards for this particular week
although he has been providing liquidity during that period.
A LP
can't attempt to reclaim , first because there were no rewards
set and the check on L269-L271 will fail because rewards
(although non-existent) are already claimed for that week
require( !ambLiquidityRewardsClaimed_[poolIdx][posKey][week], "Already claimed" );
The function LiquidityMining.claimConcentratedRewards is using the same logic with the difference that the liquidity
is concentrated and provided in a range
of 21 ticks
. The concLiquidityRewardsClaimed_[poolIdx][posKey][week] = true;
is set to true before the if (rewardsToSend > 0)
and the same scenario can happen here. If concRewardPerWeek_
are not set and are equal to 0 then rewardsToSend
are 0 and the if
is not entered , but that week
is already claimed.
function claimConcentratedRewards( address payable owner, bytes32 poolIdx, int24 lowerTick, int24 upperTick, uint32[] memory weeksToClaim ) internal { (........) // Percentage of this weeks overall in range liquidity that was provided by the user times the overall weekly rewards rewardsToSend += inRangeLiquidityOfPosition * concRewardPerWeek_[poolIdx][week] / overallInRangeLiquidity; } concLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; } if (rewardsToSend > 0) { (bool sent, ) = owner.call{value: rewardsToSend}(""); require(sent, "Sending rewards failed"); } }
My recommendation is the same as for LiquidityMining.claimAmbientRewards
.
Manual Review
ambLiquidityRewardsClaimed_[poolIdx][posKey][week]
mapping to be inside the if (rewardsToSend > 0)
so the function will look like this- ambLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; if (rewardsToSend > 0) { + ambLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; (bool sent, ) = owner.call{value: rewardsToSend}(""); require(sent, "Sending rewards failed"); }
LiquidityMining.claimConcentratedRewards()
mapping to be inside the if (rewardsToSend > 0)
so the function will look like this// Percentage of this weeks overall in range liquidity that was provided by the user times the overall weekly rewards rewardsToSend += inRangeLiquidityOfPosition * concRewardPerWeek_[poolIdx][week] / overallInRangeLiquidity; } -concLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; } if (rewardsToSend > 0) { +concLiquidityRewardsClaimed_[poolIdx][posKey][week] = true; (bool sent, ) = owner.call{value: rewardsToSend}(""); require(sent, "Sending rewards failed"); } }
Timing
#0 - 141345
2023-10-08T03:35:52Z
gov miss to set rewards
similar to https://github.com/code-423n4/2023-10-canto-findings/issues/81
gov mistake, QA is more appropriate
#1 - c4-pre-sort
2023-10-08T03:35:55Z
141345 marked the issue as primary issue
#2 - c4-pre-sort
2023-10-09T16:44:47Z
141345 marked the issue as sufficient quality report
#3 - c4-sponsor
2023-10-11T10:44:01Z
OpenCoreCH (sponsor) acknowledged
#4 - c4-sponsor
2023-10-11T10:44:06Z
OpenCoreCH marked the issue as disagree with severity
#5 - c4-judge
2023-10-18T21:51:31Z
dmvt changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-10-18T22:42:32Z
dmvt marked the issue as grade-b