The logic for reward calculation is a critical component of a staking contract. However, sometimes seemingly normal logic can be vulnerable to certain values that can break the entire protocol. In this article, I will introduce an overflow vulnerability caused by a division by 1 in a specific situation that can result in a staking contract being paralyzed.
The example contract in this article is the Apecoin staking contract. Apecoin staking is a process where users can lock up their Apecoins in a staking contract to help secure the Apecoin blockchain network and earn rewards in return. By staking Apecoins, users contribute to the validation of transactions on the blockchain and help maintain its security and stability. The Apecoin staking contract distributes rewards calculated using a formula that takes into account the staking period, the total amount of Apecoins staked, and the percentage of Apecoins staked by the user.
Users can deposit their Apecoins to the contract via the depositApeCoin() function. The depositApeCoin() function checks the amount of Apecoins. The minimum amount to deposit is 1e18. Then, it updates the status of the pool. The updating of the pool is the vulnerable point introduced in this article. After updating the pool, it updates the users' positions and the amount of Apecoins they have staked.
Users can also withdraw their staked Apecoin via the withdrawApeCoin() function. Before withdrawing, the function updates the status of the pool, calculates the rewards to be claimed, and transfers the requested amount of Apecoin. Rewards can be claimed by the claimApeCoin() function, which also includes the process of updating the pool.
Thus, the updatePool() function is a very important function in this staking contract. It takes information of a _poolId and checks block.timestamp. Then, it updates the information of a pool such as time and variables for rewards.
In the "Contract Analysis" chapter, it is described that the updatePool() function is an essential part of this contract. It is used by several functions, such as deposit, withdraw, and claim. Let's take a closer look at the line "(pool.accumulatedRewardsPerShare + (rewards * APE_COIN_PRECISION) / pool.stakedAmount).toUint96()". This line updates the pool's accumulatedRewardsPerShare by adding the weight of the newly calculated reward.
However, when the pool.stakedAmount is 1, the result of ‘(rewards * APE_COIN_PRECISION) / pool.stakedAmount)’ can exceed 2**96-1, which is the maximum value that can be represented by a uint96 data type. As a result, when the SafeCast function tries to convert the value to a uint96 data type, the process reverts, causing the staking contract to become paralyzed. While the probability of this vulnerability occurring is extremely low, it is still a possibility. In other words, it's only likely to happen right after the contract is deployed. To exploit the vulnerability, an attacker could deposit 1e18 Apecoin and immediately withdraw 1e18-1 Apecoin to make the pool.stakedAmount equal to 1.
On May 25, 2022, the Aura protocol experienced a vulnerability similar to the one described in reference . An attacker was able to cause an overflow in the updateReward() function, potentially locking all reward tokens. The return value of the _rewardPerToken() function  is based on the formula (now - lastUpdateTime) * rewardRate * 10**18 / totalSupply, with the maximum value of (now - lastUpdateTime) being 6*10**5. The rewardRate is calculated as _reward.div(rewardsDuration) , where rewardsDuration is a constant value of 604,800.
The attacker called the _notifyRewardAmount() function by transferring 1 WETH, causing the rewardRate to be approximately 10**12. By transferring a single token, the attacker was able to set totalSupply to 1. Therefore, the return value in the _rewardPerToken() function was calculated as (10**5 * 10**12 * 10**18) / 1 = 10**35. However, this value could not exceed 2**96-1 .
The following is a PoC that exploits the vulnerability by making the pool.stakedAmount equal to 1. First, Alice deposits 1e18 Apecoins to the contract. Then, she immediately withdraws 1e18-1 Apecoins, causing the pool.stakedAmount to become 1. After this, any attempt to deposit additional tokens, such as Bob's attempt, will fail due to the overflow issue in the calculation of rewards.
To mitigate the vulnerability, it is recommended to add a check to ensure that the result of the calculation does not exceed the maximum value of uint96 before updating the accumulatedRewardsPerShare variable. This can be achieved by modifying the updatePool() function as follows.
Alternatively, it is recommended to convert the result of the calculation to uint256 instead of uint96. Another way to avoid this vulnerability is to deposit tokens for rewards immediately after deploying the contract, thus avoiding the scenario where the stakedAmount of the pool is equal to 1.
In summary, the Apecoin staking contract has an overflow vulnerability caused by a division by 1 when updating the pool's accumulatedRewardsPerShare. Specifically, when pool.stakedAmount is 1, the result of ‘(rewards * APE_COIN_PRECISION) / pool.stakedAmount)’ can exceed 2**96-1, leading to a revert when converting to a uint96 data type. This vulnerability can result in the staking contract being paralyzed.