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: 8/25
Findings: 4
Award: $895.50
π Selected for report: 1
π Solo Findings: 0
π Selected for report: csanuragjain
510.9568 USDC - $510.96
csanuragjain
This can lead to loss of funds as there is no recovery function of funds stuck like this
User A creates a new promotion using createPromotion function. By mistake he provides 1 year ago value for _startTimestamp with promotion duration as 6 months
Since there is no check to see that _startTimestamp > block.timestamp so this promotion gets created
User cannot claim this promotion if they were not having promotion tokens in the 1 year old promotion period. This means promotion amount remains with contract
Even promotion creator cannot claim back his tokens since promotion end date has already passed so cancelPromotion will fail
As there is no recovery token function in contract so even contract cant transfer this token and the tokens will remain in this contract with no one able to claim those
Add below check in the createPromotion function
function createPromotion( address _ticket, IERC20 _token, uint216 _tokensPerEpoch, uint32 _startTimestamp, uint32 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { require(_startTimestamp>block.timestamp,"should be after current time"); }
#0 - PierrickGT
2021-12-09T23:16:16Z
It would indeed be an unfortunate event and we will implement this require. That being said, funds of the promotion creator would be at risk, because of an error he made, but not funds of a user, so I consider this bug as being of severity 2 (Med Risk) and not 3 (High Risk).
#1 - dmvt
2022-01-09T16:05:26Z
Per the Judge Onboarding document provided by Code423n4, this qualifies as a high risk issue. A UI bug or simple mistake could cause complete loss of funds as sponsor acknowledged.
3 β High (H): vulns have a risk of 3 and are considered βHighβ severity when assets can be stolen/lost/compromised directly (or indirectly if there is a valid attack path that does not have hand-wavy hypotheticals).
π Selected for report: gpersoon
Also found by: 0xabcc, csanuragjain, harleythedog, kenzo, leastwood
csanuragjain
There is no check to see if promotion has already ended. User can take the advantage and steal contract funds
No reward should be given for promotions which has ended
#0 - PierrickGT
2021-12-09T16:31:28Z
π Selected for report: johnnycash
Also found by: WatchPug, csanuragjain, gpersoon, gzeon, harleythedog, kemmio, kenzo, leastwood, pauliax
csanuragjain
You can steal rewards from the contract causing huge financial loss
Navigate to the contract at https://github.com/pooltogether/v4-periphery/blob/b520faea26bcf60371012f6cb246aa149abd3c7d/contracts/TwabRewards.sol
Assume User A has created a promotion with Token X
Now Attacker creates a new Promotion using createPromotion function with _numberOfEpochs as 0 and a very high value for _tokensPerEpoch and very low value for _epochDuration and token as X
function createPromotion( address _ticket, IERC20 _token, uint216 _tokensPerEpoch, uint32 _startTimestamp, uint32 _epochDuration, uint8 _numberOfEpochs ) external override returns (uint256) { _requireTicket(_ticket); uint256 _nextPromotionId = _latestPromotionId + 1; _latestPromotionId = _nextPromotionId; _promotions[_nextPromotionId] = Promotion( msg.sender, _ticket, _token, _tokensPerEpoch, _startTimestamp, _epochDuration, _numberOfEpochs ); _token.safeTransferFrom(msg.sender, address(this), _tokensPerEpoch * _numberOfEpochs); emit PromotionCreated(_nextPromotionId); return _nextPromotionId; }
Since _numberOfEpochs is 0, so user does not have to pay anything and promotion gets created
After some time, Attacker or his colleague claims the reward for his own promotion using claimRewards function
function claimRewards( address _user, uint256 _promotionId, uint256[] calldata _epochIds ) external override returns (uint256) { Promotion memory _promotion = _getPromotion(_promotionId); uint256 _rewardsAmount; uint256 _userClaimedEpochs = _claimedEpochs[_promotionId][_user]; for (uint256 index = 0; index < _epochIds.length; index++) { uint256 _epochId = _epochIds[index]; require( !_isClaimedEpoch(_userClaimedEpochs, _epochId), "TwabRewards/rewards-already-claimed" ); _rewardsAmount += _calculateRewardAmount(_user, _promotion, _epochId); _userClaimedEpochs = _updateClaimedEpoch(_userClaimedEpochs, _epochId); } _claimedEpochs[_promotionId][_user] = _userClaimedEpochs; _promotion.token.safeTransfer(_user, _rewardsAmount); emit RewardsClaimed(_promotionId, _epochIds, _user, _rewardsAmount); return _rewardsAmount; }
This internally calls the _calculateRewardAmount function which calculates the reward for Attacker. Since attacker has managed average balance for the token X so his reward amount is calculated
Finally, Contract transfer the reward to Attacker for promotion created by Attacker even though Attacker has never given any funds for this promotion (_numberOfEpochs was zero). Reward transfer will suceed as contract will have token X balance from other promotions
This means contract will lose funds and users wont be able to claim from genuine promotions
Add below checks:
function _calculateRewardAmount( address _user, Promotion memory _promotion, uint256 _epochId ) internal view returns (uint256) { require(_numberOfEpochs>=epochid, "reward exhausted for this promotion"); }
#0 - PierrickGT
2021-12-09T22:13:47Z