Lucene search

K
code423n4Code4renaCODE423N4:2023-03-WENWIN-FINDINGS-ISSUES-370
HistoryMar 09, 2023 - 12:00 a.m.

Rewards for the Staking.sol contract may be stolen via the first staker

2023-03-0900:00:00
Code4rena
github.com
12
staking contract
lot token
rewardstoken
erc20
vulnerability
reward calculation
inflation
totalsupply
getreward
mitigation

Lines of code
<https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/Staking.sol#L122&gt;

Vulnerability details

Impact

The return amount of the function rewardPerToken() may be inflated for the first in the Staking.sol contract.

Proof of Concept

The Staking.sol contract is designed for the LOT token holders to be able to stake their (native) tokens. Thus, the token holders would be able to receive corresponding rewards from ticket sales. The Staking.sol contract which is a vault-like ERC20 contract, empowers users via staking their LOT tokens and receiving their rewards by the means of rewardsToken (DAI stablecoin).
The rewardPerToken() function is where the reward calculations happen. The return amount of this function is used in _updateReward(), and earned() functions. So it’ll better to look at what is inside the function rewardPerToken():

    function rewardPerToken() public view override returns (uint256 _rewardPerToken) {
        uint256 _totalSupply = totalSupply();
        if (_totalSupply == 0) {
            return rewardPerTokenStored;
        }

        uint256 ticketsSoldSinceUpdate = lottery.nextTicketId() - lastUpdateTicketId;
        uint256 unclaimedRewards =
            LotteryMath.calculateRewards(lottery.ticketPrice(), ticketsSoldSinceUpdate, LotteryRewardType.STAKING);

        return rewardPerTokenStored + (unclaimedRewards * 1e18 / _totalSupply);
    }

As one can see, the return part of the function, for the case in which totalSupply() is not zero, contains a multiplication for 1e18. Therefore, it may enhance the return amount drastically for the cases in which totalSupply() is not in the orders of 1e18.

Consider this situation:

1 - Suppose that the contract is deployed
2 - Bob who listens to the contract bytecode, calls the stake() function with the amount 1 Wei.
3 - The _mint() function is triggered, and thus calls the _beforeTokenTransfer() hook.
4 - The function _updateReward() for Bob’s address is called.
5 - As the initial totalSupply() for the Staking contract is 0, the rewards and userRewardPerTokenPaid mappings for the key of Bob’s address become 0 respectively.
6 - Bob calls the getReward() function then.
7 - This time, _updateReward() for Bob’s address give large number, as the earned() calls the rewardPerToken()for totalSupply() of 1.
8 - the rewards[Bob’s address] would be changed to a large number, and thus Bob will receive rewards.

For detailed explanation, as the earned() function is called before the userRewardPerTokenPaid mapping for Bob’s address, the rewards amount is calculated via the previous data:

        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = currentRewardPerToken;

Tools Used

Pen & Paper

Recommended Mitigation Steps

It might be considered to initialize the contract with some token amounts or put some boundaries for stakers rewards.


The text was updated successfully, but these errors were encountered:

All reactions