Platform: Code4rena
Start Date: 09/12/2021
Pot Size: $25,000 USDC
Total HM: 12
Participants: 25
Period: 4 days
Judge: LSDan
Total Solo HM: 4
Id: 64
League: ETH
Rank: 7/25
Findings: 3
Award: $1,005.40
π Selected for report: 2
π Solo Findings: 1
π Selected for report: johnnycash
Also found by: 0x421f, cmichel, gpersoon, gzeon, harleythedog, kemmio, kenzo, sirhashalot
sirhashalot
The _epochId value is a uint256 that can be provided by the user in the _epochIds array in the important claimRewards and getRewardsAmount functions. The epochId value should be between 0 and 255, as evidenced by the bit shifting of a uint256 type in the functions _updateClaimedEpoch and _isClaimedEpoch. However, while the epochId value will not be negative because it is an unsigned integer, the edge case of epochId > 255 is not handled well. A user may submit an epochId of 300, or 300000, which can result in the following unexpected situations:
In summary, the case of epochId > 255 results in unexpected edge cases which allow a user to manipulate the rewards calculation in unexpected ways, especially if further code modifications do not examine this edge case. Edge case complexity would be substantially reduced if epochId is required to be <= 255 in the TwabRewards.sol file.
Two external functions of TwabRewards.sol allow a user to provide epochId values.
However, the _epochId value has cascading impact due to external function calls and the impact can extend to other pooltogether contracts. For example, the call flow for the claimRewards function is: claimRewards() β _calculateRewardAmount() β Ticket.getAverageBalanceBetween (out of current scope)β TwabLib.getAverageBalanceBetween (out of current scope)
require(epochIds[index] <= 255);
require(_epochEndTimestamp > _epochStartTimestamp);
to line 299, next to the existing require statement and before the uint64 casting operations, to prevent this type of manipulation#0 - PierrickGT
2021-12-13T21:08:35Z
This will be fixed in the following issue by casting _epochIds
to uint8
: https://github.com/code-423n4/2021-12-pooltogether-findings/issues/3
#1 - PierrickGT
2021-12-13T21:08:47Z
π Selected for report: sirhashalot
sirhashalot
The _calculateRewardAmount function casts epoch timestamps from uint256 to uint64 and these may overflow. The epochStartTimestamp value is a function of the user-supplied _epochId value, which could be extremely large (up to 2**255 β 1). While Solidity 0.8.x checks for overflows on arithmetic operations, it does not do so for casting β the OpenZeppelin SafeCast library offers this. The overflow condition could cause _epochStartTimestamp > _epochEndTimestamp, which the Ticket.sol getAverageBalanceBetween may not be expected to handle. The _epochStartTimestamp could overflow to have a value before the actual start of the promotion, also impacting the rewards calculation.
There are 4 uint64 casting operations in the _calculateRewardAmount function of TwabRewards.sol: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L304-L312
Manual analysis
While requiring _epochId <= 255 may help, it does not remove the issue entirely, because a very large _epochDuration value can still cause an overflow in the product (_epochDuration * _epochId) used in _epochStartTimestamp. However, other options exist:
require(_epochEndTimestamp > _epochStartTimestamp);
to line 299, next to the existing require statement and before the uint64 casting operations#0 - PierrickGT
2021-12-13T17:59:29Z
#1 - dmvt
2022-01-17T11:53:29Z
I do not consider this to be a duplicate of 58 because it describes an actual impact that, while extremely unlikely, could result in loss of funds. This also makes it a medium severity issue.
2 β Med (M): vulns have a risk of 2 and are considered βMediumβ severity when assets are not at direct risk, but the function of the protocol or its availability could be impacted, or leak value with a hypothetical attack path with stated assumptions, but external requirements.
π Selected for report: gpersoon
Also found by: 0xabcc, WatchPug, cmichel, danb, gzeon, pauliax, pmerkleplant, sirhashalot
sirhashalot
The value of _promotionEndTimestamp is checked to be greater than zero before being checked to be greater than block.timestamp. The first check is redundant, as block.timestamp is always greater than zero. Removing this unnecessary check can save gas by removing a comparison operation.
TwabRewards.sol line 255: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L255
Manual analysis
Remove the text _promotionEndTimestamp > 0 &&
from TwabRewards.sol
#0 - PierrickGT
2021-12-13T15:58:32Z
π Selected for report: robee
Also found by: GiveMeTestEther, Jujic, Meta0xNull, WatchPug, defsec, sirhashalot, ye0lde
sirhashalot
Strings are broken into 32 byte chunks for operations. Revert error strings over 32 bytes therefore consume extra gas (as documented publicly)
There are multiple examples of this gas optimization opportunity in the TwabRewards.sol file, including: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L80 https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L128 https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L177 https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L231
Manual analysis
Reducing revert error strings to under 32 bytes decreases deployment time gas and runtime gas when the revert condition is met. Alternatively, the code could be modified to use custom errors, introduced in Solidity 0.8.4: https://blog.soliditylang.org/2021/04/21/custom-errors/
#0 - PierrickGT
2021-12-13T15:57:47Z
π Selected for report: sirhashalot
Also found by: certora
44.9501 USDC - $44.95
sirhashalot
The _calculateRewardAmount()
function in TwabRewards.sol uses the uint256 type for the three variables _epochDuration
, _epochStartTimestamp
, and _epochEndTimestamp
. However, there is no need for these variable to be uint256 instead of uint64 because 1. these variables are later cast as uint64 values anyway 2. the block.timestamp value is orders of magnitude less than the uint64 max value. To expand on this second point, if the the block.timestamp values were on the same order of magnitude as the uint64 max value, then the casting of the uint256 timestamp values to uint64 could cause overflow issues because the OpenZeppelin SafeCast library is not used.
The timestamp values could even be of type uint32 (Uniswap v3 does this in places, and the max uint32 timestamp equates to the year 2106), but since the ITicket.sol contract imported by TwabRewards.sol uses uint64, it would be better to use uint64 to maintain consistency.
The uint256 variables that can be uint64 are found in TwabRewards.sol: https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol#L294-L296
Manual analysis
Make these variables uint64 for gas savings and consistency with Iticket.sol timestamps. Remove unnecessary uint64() casting when all variables in the _calculateRewardAmount()
function consistently use uint64 types.