Platform: Code4rena
Start Date: 25/01/2024
Pot Size: $16,425 USDC
Total HM: 4
Participants: 5
Period: 4 days
Judge: Alex the Entrprenerd
Total Solo HM: 1
Id: 326
League: ETH
Rank: 5/5
Findings: 0
Award: $0.00
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Data not available
https://github.com/code-423n4/2024-01-canto/blob/main/src/LendingLedger.sol#L56
Updating a market when the canto rewards for the particular block were not yet set and their value equals zero, will result in less canto rewards obtained by the lending market as the system will skip this blocks and update the market's storage to the latest block number.
The function setRewards is used by the governance to set an amount of canto rewards which is allocated for the particular block, if a block is not yet set it will return a zero value of rewards when fetched from the mapping cantoPerBlock.
/// @notice Used by governance to set the overall CANTO rewards per epoch /// @param _fromEpoch From which epoch (provided as block number) to set the rewards from /// @param _toEpoch Until which epoch (provided as block number) to set the rewards to /// @param _amountPerBlock The amount per block function setRewards( uint256 _fromEpoch, uint256 _toEpoch, uint256 _amountPerBlock ) external onlyGovernance { require(_fromEpoch % BLOCK_EPOCH == 0 && _toEpoch % BLOCK_EPOCH == 0, "Invalid block number"); for (uint256 i = _fromEpoch; i <= _toEpoch; i += BLOCK_EPOCH) { cantoPerBlock[i] = _amountPerBlock; } }
In a case when the function update_market is called and rewards for the particular block were not yet set, the system will skip the block and update the market's lastRewardBlock to the current block number as a result the market won't be able to claim any rewards, this is problematic in a case when the governance forgets to set any rewards for the upcoming blocks.
function update_market(address _market) public { require(lendingMarketWhitelist[_market], "Market not whitelisted"); MarketInfo storage market = marketInfo[_market]; if (block.number > market.lastRewardBlock) { uint256 marketSupply = lendingMarketTotalBalance[_market]; if (marketSupply > 0) { uint256 i = market.lastRewardBlock; while (i < block.number) { uint256 epoch = (i / BLOCK_EPOCH) * BLOCK_EPOCH; // Rewards and voting weights are aligned on a weekly basis uint256 nextEpoch = i + BLOCK_EPOCH; uint256 blockDelta = Math.min(nextEpoch, block.number) - i; uint256 cantoReward = (blockDelta * cantoPerBlock[epoch] * gaugeController.gauge_relative_weight_write(_market, epoch)) / 1e18; market.accCantoPerShare += uint128((cantoReward * 1e18) / marketSupply); market.secRewardsPerShare += uint128((blockDelta * 1e18) / marketSupply); // TODO: Scaling i += blockDelta; } } market.lastRewardBlock = uint64(block.number); } }
Manual review.
My recommendation here is to apply a defensive mechanism which indicates that the rewards for this particular block were already set and that the market can be further updated:
/// @notice Used by governance to set the overall CANTO rewards per epoch /// @param _fromEpoch From which epoch (provided as block number) to set the rewards from /// @param _toEpoch Until which epoch (provided as block number) to set the rewards to /// @param _amountPerBlock The amount per block function setRewards( uint256 _fromEpoch, uint256 _toEpoch, uint256 _amountPerBlock, + bool _set ) external onlyGovernance { require(_fromEpoch % BLOCK_EPOCH == 0 && _toEpoch % BLOCK_EPOCH == 0, "Invalid block number"); for (uint256 i = _fromEpoch; i <= _toEpoch; i += BLOCK_EPOCH) { cantoPerBlock[i] = _amountPerBlock; + set[i] = _set } }
function update_market(address _market) public { require(lendingMarketWhitelist[_market], "Market not whitelisted"); MarketInfo storage market = marketInfo[_market]; if (block.number > market.lastRewardBlock) { uint256 marketSupply = lendingMarketTotalBalance[_market]; if (marketSupply > 0) { uint256 i = market.lastRewardBlock; while (i < block.number) { + if(!set[i]) revert(); uint256 epoch = (i / BLOCK_EPOCH) * BLOCK_EPOCH; // Rewards and voting weights are aligned on a weekly basis uint256 nextEpoch = i + BLOCK_EPOCH; uint256 blockDelta = Math.min(nextEpoch, block.number) - i; uint256 cantoReward = (blockDelta * cantoPerBlock[epoch] * gaugeController.gauge_relative_weight_write(_market, epoch)) / 1e18; market.accCantoPerShare += uint128((cantoReward * 1e18) / marketSupply); market.secRewardsPerShare += uint128((blockDelta * 1e18) / marketSupply); // TODO: Scaling i += blockDelta; } } market.lastRewardBlock = uint64(block.number); } }
Other
#0 - c4-judge
2024-01-30T15:20:25Z
GalloDaSballo marked the issue as primary issue
#1 - c4-sponsor
2024-01-31T10:33:19Z
OpenCoreCH (sponsor) acknowledged
#2 - OpenCoreCH
2024-01-31T10:34:59Z
This is technically true, but seems to be rather an admin / governance mistake (forgetting to set rewards) to me and the user will be aware of it (we display the current rewards in the frontend). I think there is not really a perfect solution for it, the proposal would introduce a new risk, as all markets will be DoSed when the rewards are not set (which is even worse imo)
#3 - stormy0998
2024-01-31T21:17:27Z
Totally agree with you, to be honest the only rather good solution here which won't result in DoS is for the function to return when cantoPerBlock equals zero and rewards were not yet set for this block. However while this won't affect the system in DoS way, it will let the user claim & syncLedger based on the old market accCantoPerShare value.
#4 - c4-judge
2024-02-01T10:11:43Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#5 - GalloDaSballo
2024-02-01T10:11:55Z
Downgrading to QA as Admin Mistake
#6 - c4-judge
2024-02-01T10:24:10Z
GalloDaSballo marked the issue as grade-b