Canto Invitational - Stormy's results

Incentivization Primitive for Real World Assets on Canto.

General Information

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

Canto

Findings Distribution

Researcher Performance

Rank: 5/5

Findings: 0

Award: $0.00

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: Stormy, bin2chen, erebus, xuwinnie

Labels

bug
QA (Quality Assurance)
downgraded by judge
grade-b
primary issue
sponsor acknowledged
Q-01

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2024-01-canto/blob/main/src/LendingLedger.sol#L56

Vulnerability details

Impact

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.

Proof of Concept

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);
        }
    }

Tools Used

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);
        }
    }

Assessed type

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter