Platform: Code4rena
Start Date: 06/03/2023
Pot Size: $36,500 USDC
Total HM: 8
Participants: 93
Period: 3 days
Judge: cccz
Total Solo HM: 3
Id: 218
League: ETH
Rank: 45/93
Findings: 1
Award: $81.41
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Rolezn
Also found by: 0x1f8b, 0x6980, 0xSmartContract, 0xhacksmithh, 0xnev, Haipls, Inspectah, JCN, LethL, Madalad, MiniGlome, Pheonix, Rageur, RaymondFam, ReyAdmirado, SAAJ, Sathish9098, adriro, air, arialblack14, atharvasama, c3phas, ch0bu, ddimitrov22, descharre, hunter_w3b, igingu, matrix_0wl, rokso, saneryee, schrodinger, slvDev, volodya, yongskiws
81.411 USDC - $81.41
There are multiple gas saving opportunities are found and all of these has impacted code linked, a description of issue and recommendation. I have also added git diff for recommendation for each issue.
Total Findings = 12
if
condition to save gasunlockTimestamp
and initialize in constructorThis is different than what automated tool has reported.
Code: Lottery.sol#L121-L130
Issue: Linked code is using array length at multiple places in function and not just in for loop
.
Recommendation: Consider storing length of drawIds.length
in local variable and it can be used as replacement of drawIds.length
and tickets.length
after if
condition.
diff --git a/src/Lottery.sol b/src/Lottery.sol index 28d3477..5ae6fd9 100644 --- a/src/Lottery.sol +++ b/src/Lottery.sol @@ -118,16 +118,17 @@ contract Lottery is ILottery, Ticket, LotterySetup, ReferralSystem, RNSourceCont requireJackpotInitialized returns (uint256[] memory ticketIds) { - if (drawIds.length != tickets.length) { - revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length); + uint256 len = drawIds.length; + if (len != tickets.length) { + revert DrawsAndTicketsLenMismatch(len, tickets.length); } - ticketIds = new uint256[](tickets.length); - for (uint256 i = 0; i < drawIds.length; ++i) { + ticketIds = new uint256[](len); + for (uint256 i = 0; i < len; ++i) { ticketIds[i] = registerTicket(drawIds[i], tickets[i], frontend, referrer); } - referralRegisterTickets(currentDraw, referrer, msg.sender, tickets.length); - frontendDueTicketSales[frontend] += tickets.length; - rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length); + referralRegisterTickets(currentDraw, referrer, msg.sender,len); + frontendDueTicketSales[frontend] += len; + rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * len); }
referrerTicketCount
can be stored in local variableCode: ReferralSystem.sol#L62-L66
Issue: Code is reading mapping storage value unclaimedTickets[currentDraw][referrer].referrerTicketCount
multiple times.
Recommendation: Consider storing aforementioned storage variable in local variable and reference local variable at other places inside function.
diff --git a/src/ReferralSystem.sol b/src/ReferralSystem.sol index d74ced7..fa59047 100644 --- a/src/ReferralSystem.sol +++ b/src/ReferralSystem.sol @@ -59,10 +59,10 @@ abstract contract ReferralSystem is IReferralSystem { { if (referrer != address(0)) { uint256 minimumEligible = minimumEligibleReferrals[currentDraw]; - if (unclaimedTickets[currentDraw][referrer].referrerTicketCount + numberOfTickets >= minimumEligible) { - if (unclaimedTickets[currentDraw][referrer].referrerTicketCount < minimumEligible) { - totalTicketsForReferrersPerDraw[currentDraw] += - unclaimedTickets[currentDraw][referrer].referrerTicketCount; + uint256 referrerTicketCount = unclaimedTickets[currentDraw][referrer].referrerTicketCount; + if (referrerTicketCount + numberOfTickets >= minimumEligible) { + if (referrerTicketCount < minimumEligible) { + totalTicketsForReferrersPerDraw[currentDraw] += referrerTicketCount; } totalTicketsForReferrersPerDraw[currentDraw] += numberOfTickets; }
status
can be stored in local variableCode: RNSourceBase.sol#L34-L39
Issue: For a successful, which will be the case mostly, call to fulfill()
require reading request status
multiple times from storage.
Recommendation: Consider storing requests[requestId].status in local variable.
diff --git a/src/RNSourceBase.sol b/src/RNSourceBase.sol index 616fba8..5fb8fbc 100644 --- a/src/RNSourceBase.sol +++ b/src/RNSourceBase.sol @@ -31,10 +31,11 @@ abstract contract RNSourceBase is IRNSource { /// @dev Must be called in the deriving contract's `requestRandomnessFromUnderlyingSource` function function fulfill(uint256 requestId, uint256 randomNumber) internal { - if (requests[requestId].status == RequestStatus.None) { + RequestStatus status = requests[requestId].status; + if (status == RequestStatus.None) { revert RequestNotFound(requestId); } - if (requests[requestId].status == RequestStatus.Fulfilled) { + if (status == RequestStatus.Fulfilled) { revert RequestAlreadyFulfilled(requestId); }
Code: LotterySetup.sol#L170-L173
Issue: Code is calculating reward and then checking if ((rewards[winTier] % divisor) != 0)
. It may revert is condition is true and gas to calculate reward is wasted.
Recommendation: Consider moving if
condition before calculating reward.
diff --git a/src/LotterySetup.sol b/src/LotterySetup.sol index 9c70086..d8b11f4 100644 --- a/src/LotterySetup.sol +++ b/src/LotterySetup.sol @@ -167,10 +167,10 @@ contract LotterySetup is ILotterySetup { } uint256 divisor = 10 ** (IERC20Metadata(address(rewardToken)).decimals() - 1); for (uint8 winTier = 1; winTier < selectionSize; ++winTier) { - uint16 reward = uint16(rewards[winTier] / divisor); if ((rewards[winTier] % divisor) != 0) { revert InvalidFixedRewardSetup(); } + uint16 reward = uint16(rewards[winTier] / divisor); packed |= uint256(reward) << (winTier * 16); } }
Code: ReferralSystem.sol#L145
Issue: Code is reading unclaimedTickets[drawId][msg.sender]
and based on some condition is it setting one of the struct param. Now code is reading unclaimedTickets[drawId][msg.sender]
again which make sense if you are going to use updated param later which is not the case here. So there is no need to read storage variable again in this case.
Recommendation: Remove _unclaimedTickets = unclaimedTickets[drawId][msg.sender]
line
diff --git a/src/ReferralSystem.sol b/src/ReferralSystem.sol index d74ced7..a3ac663 100644 --- a/src/ReferralSystem.sol +++ b/src/ReferralSystem.sol @@ -142,7 +142,6 @@ abstract contract ReferralSystem is IReferralSystem { unclaimedTickets[drawId][msg.sender].referrerTicketCount = 0; } - _unclaimedTickets = unclaimedTickets[drawId][msg.sender]; if (_unclaimedTickets.playerTicketCount > 0) { claimedReward += playerRewardsPerDrawForOneTicket[drawId] * _unclaimedTickets.playerTicketCount; unclaimedTickets[drawId][msg.sender].playerTicketCount = 0;
Code: RNSourceController.sol#L73
Issue: Storage variable failedSequentialAttempts
is being used during emitting event. Before emitting event, code is updating storage variable and storing value in local variable failedAttempts
.
Recommendation: Local variable failedAttempts
can be used to replace storage variable when emitting event.
diff --git a/src/RNSourceController.sol b/src/RNSourceController.sol index 2bd8cfc..28f317b 100644 --- a/src/RNSourceController.sol +++ b/src/RNSourceController.sol @@ -70,7 +70,7 @@ abstract contract RNSourceController is Ownable2Step, IRNSourceController { maxFailedAttemptsReachedAt = block.timestamp; } - emit Retry(source, failedSequentialAttempts); + emit Retry(source, failedAttempts); requestRandomNumberFromSource(); }
if
condition to save gasCode: RNSourceController.sol#L93-L95
Issue: There are 2 conditions which decides if logic has notEnoughFailedAttempts
. if
statement has 2 OR conditions and code is calculating both conditions before checking those in if
but there is no need to calculate both condition before hand, as 2nd condition will only execute if 1st condition is false
.
Recommendation: Use both condition expressions directly in if
statement, doing this will save gas for not calculating 2nd condition unnecessarily and saves gas as results of expressions are not stored in local variables.
diff --git a/src/RNSourceController.sol b/src/RNSourceController.sol index 2bd8cfc..7e5d524 100644 --- a/src/RNSourceController.sol +++ b/src/RNSourceController.sol @@ -90,9 +90,7 @@ abstract contract RNSourceController is Ownable2Step, IRNSourceController { if (address(newSource) == address(0)) { revert RNSourceZeroAddress(); } - bool notEnoughRetryInvocations = failedSequentialAttempts < maxFailedAttempts; - bool notEnoughTimeReachingMaxFailedAttempts = block.timestamp < maxFailedAttemptsReachedAt + maxRequestDelay; - if (notEnoughRetryInvocations || notEnoughTimeReachingMaxFailedAttempts) { + if ((failedSequentialAttempts < maxFailedAttempts) || (block.timestamp < maxFailedAttemptsReachedAt + maxRequestDelay)) { revert NotEnoughFailedAttempts(); } source = newSource;
Code: RNSourceBase.sol#L27
Issue: Code is initializing a RandomnessRequest
struct with both, status
and randomNumber
, of it's params and storing it in storage but only status
param needs update.
Recommendation: To avoid setting unnecessary params consider updating only required struct param directly compare to setting whole struct to storage.
diff --git a/src/RNSourceBase.sol b/src/RNSourceBase.sol index 616fba8..5b22369 100644 --- a/src/RNSourceBase.sol +++ b/src/RNSourceBase.sol @@ -24,7 +24,7 @@ abstract contract RNSourceBase is IRNSource { revert requestIdAlreadyExists(requestId); } - requests[requestId] = RandomnessRequest({ status: RequestStatus.Pending, randomNumber: 0 }); + requests[requestId].status = RequestStatus.Pending; emit RequestedRandomNumber(msg.sender, requestId); }
unlockTimestamp
and initialize in constructorCode: StakedTokenLock.sol#L39
Issue: Withdraw() function has if
condition which is checking block.timestamp < depositDeadline + lockDuration
. depositDeadline and lockDuration both are storage and immutable variables. So it is possible to store sum of these 2 as another immutable variable.
Recommendation: Consider adding a new immutable variable which holds sum of depositDeadline
and lockDuration
and use this new variable in if
condition in withdraw()
.
diff --git a/src/staking/StakedTokenLock.sol b/src/staking/StakedTokenLock.sol index f756d4d..58dfdb6 100644 --- a/src/staking/StakedTokenLock.sol +++ b/src/staking/StakedTokenLock.sol @@ -11,6 +11,7 @@ contract StakedTokenLock is IStakedTokenLock, Ownable2Step { IERC20 public immutable override rewardsToken; uint256 public immutable override depositDeadline; uint256 public immutable override lockDuration; + uint256 public immutable unlockTimestamp; uint256 public override depositedBalance; constructor(address _stakedToken, uint256 _depositDeadline, uint256 _lockDuration) { @@ -19,6 +20,7 @@ contract StakedTokenLock is IStakedTokenLock, Ownable2Step { rewardsToken = stakedToken.rewardsToken(); depositDeadline = _depositDeadline; lockDuration = _lockDuration; + unlockTimestamp = depositDeadline + lockDuration; } function deposit(uint256 amount) external override onlyOwner { @@ -36,7 +38,7 @@ contract StakedTokenLock is IStakedTokenLock, Ownable2Step { function withdraw(uint256 amount) external override onlyOwner { // slither-disable-next-line timestamp - if (block.timestamp > depositDeadline && block.timestamp < depositDeadline + lockDuration) { + if (block.timestamp > depositDeadline && block.timestamp < unlockTimestamp) { revert LockPeriodOngoing(); }
Code: LotterySetup.sol#L140-L147
Issue: minInitialPot
is initialized as minInitialPot = 4 * tokenUnit
during deployment of contract.
assert(initialPot > 0)
check is not required because
Recommendation: Remove assert(initialPot > 0)
check.
diff --git a/src/LotterySetup.sol b/src/LotterySetup.sol index 9c70086..2663af4 100644 --- a/src/LotterySetup.sol +++ b/src/LotterySetup.sol @@ -138,14 +138,12 @@ contract LotterySetup is ILotterySetup { revert FinalizingInitialPotBeforeDeadline(); } uint256 raised = rewardToken.balanceOf(address(this)); + if (raised < minInitialPot) { revert RaisedInsufficientFunds(raised); } initialPot = raised; - // must hold after this call, this will be used as a check that jackpot is initialized - assert(initialPot > 0); - emit InitialPotPeriodFinalized(raised); }
Code: TicketUtils.sol#L54-L75
Issue: There are two for loops in reconstructTicket()
function. One loop is preparing an array numbers
based on some calculations. Second loop is using looping over numbers
and calculating elements for boolean array selected
. It is possible to merge these two for loops into one as each iteration can calculate currentNumber
and use it right away to calculate it is selected or not.
Recommendation: Consider merging two for loops into one.
diff --git a/src/TicketUtils.sol b/src/TicketUtils.sol index be447aa..202ec45 100644 --- a/src/TicketUtils.sol +++ b/src/TicketUtils.sol @@ -51,19 +51,14 @@ library TicketUtils { { /// Ticket must contain unique numbers, so we are using smaller selection count in each iteration /// It basically means that, once `x` numbers are selected our choice is smaller for `x` numbers - uint8[] memory numbers = new uint8[](selectionSize); + bool[] memory selected = new bool[](selectionMax); uint256 currentSelectionCount = uint256(selectionMax); for (uint256 i = 0; i < selectionSize; ++i) { - numbers[i] = uint8(randomNumber % currentSelectionCount); + uint8 currentNumber = uint8(randomNumber % currentSelectionCount); randomNumber /= currentSelectionCount; currentSelectionCount--; - } - bool[] memory selected = new bool[](selectionMax); - - for (uint256 i = 0; i < selectionSize; ++i) { - uint8 currentNumber = numbers[i]; // check current selection for numbers smaller than current and increase if needed for (uint256 j = 0; j <= currentNumber; ++j) { if (selected[j]) {
Code: Lottery.sol#L260-L264
Issue: claimWinningTicket()
function is calling claimable
and storing returned variable into two local variables. Second variable is being assigned value but never used.
Recommendation: Consider removing local variable winTier
and it's assignment.
diff --git a/src/Lottery.sol b/src/Lottery.sol index 28d3477..14eb1b3 100644 --- a/src/Lottery.sol +++ b/src/Lottery.sol @@ -257,8 +257,7 @@ contract Lottery is ILottery, Ticket, LotterySetup, ReferralSystem, RNSourceCont } function claimWinningTicket(uint256 ticketId) private onlyTicketOwner(ticketId) returns (uint256 claimedAmount) { - uint256 winTier; - (claimedAmount, winTier) = this.claimable(ticketId); + (claimedAmount,) = this.claimable(ticketId); if (claimedAmount == 0) { revert NothingToClaim(ticketId); }
#0 - c4-judge
2023-03-12T14:26:59Z
thereksfour marked the issue as grade-a
#1 - c4-sponsor
2023-03-14T13:11:26Z
TutaRicky marked the issue as sponsor confirmed