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: 4/93
Findings: 5
Award: $1,307.28
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: Cyfrin
Also found by: Yukti_Chinta, adriro, anodaram, auditor0517, bin2chen, gogo, minhtrng
619.3361 USDC - $619.34
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotteryMath.sol#L35-L56
The function calculateNewProfit
present in the LotteryMath
library is used when finalizing the current draw in the Lottery to track and update the currentNetProfit
variable in the contract.
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotteryMath.sol#L35-L56
function calculateNewProfit( int256 oldProfit, uint256 ticketsSold, uint256 ticketPrice, bool jackpotWon, uint256 fixedJackpotSize, uint256 expectedPayout ) internal pure returns (int256 newProfit) { uint256 ticketsSalesToPot = (ticketsSold * ticketPrice).getPercentage(TICKET_PRICE_TO_POT); newProfit = oldProfit + int256(ticketsSalesToPot); uint256 expectedRewardsOut = jackpotWon ? calculateReward(oldProfit, fixedJackpotSize, fixedJackpotSize, ticketsSold, true, expectedPayout) : calculateMultiplier(calculateExcessPot(oldProfit, fixedJackpotSize), ticketsSold, expectedPayout) * ticketsSold * expectedPayout; newProfit -= int256(expectedRewardsOut); }
When jackpotWon == false
, the function calls calculateMultiplier
, which returns a scaled percentage, and misses to call getPercentage
to scale down the value.
In the case that the jackpot isn't won in the current draw, the profit calculation will be wrongly scaled by a factor of PERCENTAGE_BASE
(100_000) and will result in higher values that are offsetted by several orders of magnitude.
This error will cascade into rewards, as the currentNetProfit
variable is used in the drawRewardSize
function, which is used to calculate reward values and bonuses:
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L238-L247
function drawRewardSize(uint128 drawId, uint8 winTier) private view returns (uint256 rewardSize) { return LotteryMath.calculateReward( currentNetProfit, fixedReward(winTier), fixedReward(selectionSize), ticketsSold[drawId], winTier == selectionSize, expectedPayout ); }
In the following test, a user purchases just 10 tickets that aren't the winning ticket (meaning no jackpot is won). The updated net profit ends up being -379965 DAI!
contract AuditTest is LotteryTestBase { function test_LotteryMath_calculateNewProfit_WrongCalculationWhenJackpotNotWon() public { // At start current profit is 0 int256 currentProfit = lottery.currentNetProfit(); assertEq(currentProfit, 0); // User buys some tickets address user = makeAddr("user"); vm.startPrank(user); buySameTickets(lottery.currentDraw(), uint120(0x0F), address(0), 10); vm.stopPrank(); // Finalize draw with no jackpot finalizeDraw(0x01020304); // Net profit is now -379965 * (10 ** 18)! currentProfit = lottery.currentNetProfit(); assertEq(currentProfit, -379_965 ether); } }
The calculation should use getPercentage
to correctly scale down the percentage value:
function calculateNewProfit( int256 oldProfit, uint256 ticketsSold, uint256 ticketPrice, bool jackpotWon, uint256 fixedJackpotSize, uint256 expectedPayout ) internal pure returns (int256 newProfit) { uint256 ticketsSalesToPot = (ticketsSold * ticketPrice).getPercentage(TICKET_PRICE_TO_POT); newProfit = oldProfit + int256(ticketsSalesToPot); uint256 expectedRewardsOut = jackpotWon ? calculateReward(oldProfit, fixedJackpotSize, fixedJackpotSize, ticketsSold, true, expectedPayout) : (ticketsSold * expectedPayout).getPercentage(calculateMultiplier(calculateExcessPot(oldProfit, fixedJackpotSize), ticketsSold, expectedPayout)); newProfit -= int256(expectedRewardsOut); }
#0 - c4-judge
2023-03-11T10:35:40Z
thereksfour marked the issue as duplicate of #219
#1 - c4-judge
2023-03-19T10:23:41Z
thereksfour marked the issue as satisfactory
#2 - c4-judge
2023-03-21T02:20:51Z
thereksfour changed the severity to 3 (High Risk)
🌟 Selected for report: sashik_eth
Also found by: 0xbepresent, Dug, Haipls, MadWookie, adriro, hl_, horsefacts, peanuts
148.6407 USDC - $148.64
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Ticket.sol#L12
A bad actor can trick a user into buying an unclaimed ticket and frontrun the selling or transfer of the NFT to claim the rewards associated with the ticket before the original transaction.
Given the described scenario, a bad actor can frontrun the transaction associated with the exchange of an unclaimed ticket with a transaction to claim the rewards associated with the ticket in the lottery. The malicious user will end up with both the ticket winnings and the sale revenue.
Here "bad actor" is selling the unclaimed ticket NFT and "buyer" wishes to purchase it.
Quick example:
abstract contract Ticket is ITicket, ERC721 { uint256 constant TRANSFER_COOLDOWN = 1 hours; uint256 public override nextTicketId; mapping(uint256 => ITicket.TicketInfo) public override ticketsInfo; mapping(uint256 => uint256) private claimedAt; // solhint-disable-next-line no-empty-blocks constructor() ERC721("Wenwin Lottery Ticket", "WLT") { } function markAsClaimed(uint256 ticketId) internal { ticketsInfo[ticketId].claimed = true; claimedAt[ticketId] = block.timestamp; } function mint(address to, uint128 drawId, uint120 combination) internal returns (uint256 ticketId) { ticketId = nextTicketId++; ticketsInfo[ticketId] = TicketInfo(drawId, combination, false); _mint(to, ticketId); } function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal override { super._beforeTokenTransfer(from, to, tokenId, batchSize); require(block.timestamp >= claimedAt[tokenId] + TRANSFER_COOLDOWN); } }
#0 - c4-judge
2023-03-11T10:10:50Z
thereksfour marked the issue as primary issue
#1 - c4-judge
2023-03-11T11:14:23Z
thereksfour marked the issue as duplicate of #425
#2 - c4-judge
2023-03-19T10:06:17Z
thereksfour marked the issue as satisfactory
305.845 USDC - $305.84
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotterySetup.sol#L164-L176
When the lottery is initialized, fixed rewards are tightly packed in a 256 bit word. This is implemented in the packFixedRewards
function:
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotterySetup.sol#L164-L176
function packFixedRewards(uint256[] memory rewards) private view returns (uint256 packed) { if (rewards.length != (selectionSize) || rewards[0] != 0) { revert InvalidFixedRewardSetup(); } 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(); } packed |= uint256(reward) << (winTier * 16); } }
As shown in the previous snippet, the code will first divide the rewards by 10 ** (decimals - 1)
, meaning it will keep only the first decimal and discard any other precision, and then cast that result as an uint16
. This casting will potentially overflow
if the result is bigger than the max capacity of uint16
, which doesn't represent a big value considering the protocol setup.
Fixed rewards greater than 6553.5 ether
will silently overflow and wrap around. If the reward token is DAI or any other similar stable coin, then 6553.5 USD can't be considered a very high value and there is a real possibility of setting a value above that, which will end up in an overflow that won't raise any error.
The following test illustrates the issue. Fixed reward for tier 1 is just on the limit (6553.5
ether) and will be ok. Reward for tier 2 (6553.6
ether) will overflow and wrap around to zero.
contract AuditTest is LotteryTestBase { function test_LotterySetup_packFixedRewards_FixedRewardOverflow() public { uint256[] memory fixedRewards = new uint256[](SELECTION_SIZE); fixedRewards[0] = 0; fixedRewards[1] = 6553.5 ether; fixedRewards[2] = 6553.6 ether; firstDrawAt = block.timestamp + 3 * PERIOD; Lottery lottery = new Lottery( LotterySetupParams( rewardToken, LotteryDrawSchedule(firstDrawAt, PERIOD, COOL_DOWN_PERIOD), TICKET_PRICE, SELECTION_SIZE, SELECTION_MAX, EXPECTED_PAYOUT, fixedRewards ), playerRewardFirstDraw, playerRewardDecrease, rewardsToReferrersPerDraw, MAX_RN_FAILED_ATTEMPTS, MAX_RN_REQUEST_DELAY ); // Fixed rewards for tier 1 are ok... uint256 winTier1Reward = lottery.currentRewardSize(1); assertEq(winTier1Reward, fixedRewards[1]); // Fixed rewards for tier 2 overflowed to zero uint256 winTier2Reward = lottery.currentRewardSize(2); assertEq(winTier2Reward, 0); } }
If the protocol decides to pack rewards in smaller types, then ensure any overflow causes an explicit error by using, for example, the OpenZeppelin library SafeCast
:
function packFixedRewards(uint256[] memory rewards) private view returns (uint256 packed) { if (rewards.length != (selectionSize) || rewards[0] != 0) { revert InvalidFixedRewardSetup(); } uint256 divisor = 10 ** (IERC20Metadata(address(rewardToken)).decimals() - 1); for (uint8 winTier = 1; winTier < selectionSize; ++winTier) { uint16 reward = SafeCast.toUint16(rewards[winTier] / divisor); if ((rewards[winTier] % divisor) != 0) { revert InvalidFixedRewardSetup(); } packed |= uint256(reward) << (winTier * 16); } }
#0 - c4-judge
2023-03-11T10:16:47Z
thereksfour marked the issue as primary issue
#1 - c4-sponsor
2023-03-13T14:00:50Z
rand0c0des marked the issue as sponsor acknowledged
#2 - c4-sponsor
2023-03-13T14:01:18Z
rand0c0des marked the issue as sponsor confirmed
#3 - c4-judge
2023-03-19T10:04:52Z
thereksfour marked issue #245 as primary and marked this issue as a duplicate of 245
#4 - c4-judge
2023-03-19T10:04:52Z
thereksfour marked the issue as satisfactory
🌟 Selected for report: adriro
Also found by: 0x1f8b, 0xAgro, 0xSmartContract, 0xfuje, 0xkazim, 0xnev, Aymen0909, Bason, Cyfrin, DadeKuma, LethL, Madalad, MohammedRizwan, Rolezn, SAAJ, SunSec, Udsen, Yukti_Chinta, ast3ros, bin2chen, brgltd, bshramin, btk, bugradar, catellatech, cryptostellar5, descharre, dontonka, erictee, fatherOfBlocks, georgits, glcanvas, hl_, horsefacts, igingu, juancito, lukris02, martin, nadin, nomoi, peanuts, pipoca, sakshamguruji, seeu, slvDev, tnevler, zaskoh
220.7385 USDC - $220.74
The following Certora rule proves that reconstructed tickets from a random source are valid within the accepted range of selectionSize
and selectionMax
parameters.
methods { isValidTicket(uint256,uint8,uint8) returns (bool) envfree reconstructTicket(uint256,uint8,uint8) returns (uint120) envfree } rule reconstructedTicketIsValid() { uint256 random; uint8 selectionSize; uint8 selectionMax; require selectionMax <= 120; require selectionSize <= 16 || selectionSize <= (selectionMax -1); uint256 ticket = reconstructTicket(random, selectionSize, selectionMax); assert isValidTicket(ticket, selectionSize, selectionMax), "reconstructed ticket is not valid"; }
Issue | Instances | |
---|---|---|
NC-1 | Import declarations should import specific symbols | 53 |
NC-2 | Use named parameters for mapping type declarations | 17 |
NC-3 | Refactor common code across functions | 1 |
NC-4 | Unused local variables | 1 |
NC-5 | Use a modifier for access control | 1 |
NC-6 | Missing event for important parameter change | 2 |
Prefer import declarations that specify the symbol(s) using the form import {SYMBOL} from "SomeContract.sol"
rather than importing the whole file
Instances (53):
File: src/Lottery.sol 5: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6: import "@openzeppelin/contracts/utils/math/Math.sol"; 7: import "src/ReferralSystem.sol"; 8: import "src/RNSourceController.sol"; 9: import "src/staking/Staking.sol"; 10: import "src/LotterySetup.sol"; 11: import "src/TicketUtils.sol";
File: src/LotteryMath.sol 5: import "src/interfaces/ILottery.sol"; 6: import "src/PercentageMath.sol";
File: src/LotterySetup.sol 5: import "@openzeppelin/contracts/utils/math/Math.sol"; 6: import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; 7: import "src/PercentageMath.sol"; 8: import "src/LotteryToken.sol"; 9: import "src/interfaces/ILotterySetup.sol"; 10: import "src/Ticket.sol";
File: src/LotteryToken.sol 5: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6: import "src/interfaces/ILotteryToken.sol"; 7: import "src/LotteryMath.sol";
File: src/RNSourceBase.sol 5: import "src/interfaces/IRNSource.sol";
File: src/RNSourceController.sol 5: import "@openzeppelin/contracts/access/Ownable2Step.sol"; 6: import "src/interfaces/IRNSource.sol"; 7: import "src/interfaces/IRNSourceController.sol";
File: src/ReferralSystem.sol 5: import "@openzeppelin/contracts/utils/math/Math.sol"; 6: import "src/interfaces/IReferralSystem.sol"; 7: import "src/PercentageMath.sol";
File: src/Ticket.sol 5: import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6: import "src/interfaces/ITicket.sol";
File: src/VRFv2RNSource.sol 5: import "@chainlink/contracts/src/v0.8/VRFV2WrapperConsumerBase.sol"; 6: import "src/interfaces/IVRFv2RNSource.sol"; 7: import "src/RNSourceBase.sol";
File: src/interfaces/ILottery.sol 5: import "src/interfaces/ILotterySetup.sol"; 6: import "src/interfaces/IRNSourceController.sol"; 7: import "src/interfaces/ITicket.sol"; 8: import "src/interfaces/IReferralSystem.sol";
File: src/interfaces/ILotterySetup.sol 5: import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6: import "src/interfaces/ITicket.sol";
File: src/interfaces/ILotteryToken.sol 5: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
File: src/interfaces/IRNSourceController.sol 5: import "@openzeppelin/contracts/access/Ownable2Step.sol"; 6: import "src/interfaces/IRNSource.sol";
File: src/interfaces/IReferralSystem.sol 5: import "src/interfaces/ILotteryToken.sol";
File: src/interfaces/ITicket.sol 5: import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
File: src/interfaces/IVRFv2RNSource.sol 5: import "src/interfaces/IRNSource.sol";
File: src/staking/StakedTokenLock.sol 5: import "@openzeppelin/contracts/access/Ownable2Step.sol"; 6: import "src/staking/interfaces/IStakedTokenLock.sol"; 7: import "src/staking/interfaces/IStaking.sol";
File: src/staking/Staking.sol 5: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7: import "src/interfaces/ILottery.sol"; 8: import "src/LotteryMath.sol"; 9: import "src/staking/interfaces/IStaking.sol";
File: src/staking/interfaces/IStakedTokenLock.sol 5: import "src/staking/interfaces/IStaking.sol";
File: src/staking/interfaces/IStaking.sol 5: import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6: import "src/interfaces/ILottery.sol";
Consider using named parameters in mappings (e.g. mapping(address account => uint256 balance)
) to improve readability. This feature is present since Solidity 0.8.18
Instances (17):
File: src/Lottery.sol 26: mapping(address => uint256) private frontendDueTicketSales; 27: mapping(uint128 => mapping(uint120 => uint256)) private unclaimedCount; 27: mapping(uint128 => mapping(uint120 => uint256)) private unclaimedCount; 36: mapping(uint128 => uint120) public override winningTicket; 37: mapping(uint128 => mapping(uint8 => uint256)) public override winAmount; 37: mapping(uint128 => mapping(uint8 => uint256)) public override winAmount; 39: mapping(uint128 => uint256) public override ticketsSold;
File: src/RNSourceBase.sol 9: mapping(uint256 => RandomnessRequest) internal requests;
File: src/ReferralSystem.sol 17: mapping(uint128 => mapping(address => UnclaimedTicketsData)) public override unclaimedTickets; 17: mapping(uint128 => mapping(address => UnclaimedTicketsData)) public override unclaimedTickets; 19: mapping(uint128 => uint256) public override totalTicketsForReferrersPerDraw; 21: mapping(uint128 => uint256) public override referrerRewardPerDrawForOneTicket; 23: mapping(uint128 => uint256) public override playerRewardsPerDrawForOneTicket; 25: mapping(uint128 => uint256) public override minimumEligibleReferrals;
File: src/Ticket.sol 14: mapping(uint256 => ITicket.TicketInfo) public override ticketsInfo;
File: src/staking/Staking.sol 19: mapping(address => uint256) public override userRewardPerTokenPaid; 20: mapping(address => uint256) public override rewards;
claimRewards
and unclaimedRewards
functions in the Lottery
contract have a lot of duplicate functionality. Consider refactoring common code in a private function.Unused variables should be removed.
File: src/Lottery.sol 260: uint256 winTier;
Consider using a modifier to implement access control instead of inlining the condition/requirement in the function's body.
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/RNSourceController.sol#L46-L49
function onRandomNumberFulfilled(uint256 randomNumber) external override { if (msg.sender != address(source)) { revert RandomNumberFulfillmentUnauthorized(); }
Important parameter or configuration changes should trigger an event to allow being tracked off-chain.
Instances (2):
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/StakedTokenLock.sol#L24
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/StakedTokenLock.sol#L37
Issue | Instances | |
---|---|---|
L-1 | Contract files should define a locked compiler version | 2 |
L-2 | claimable function doesn't validate ticket draw is finished | 1 |
L-3 | DRAWS_PER_YEAR assumes one draw per week | 1 |
L-4 | Simplify unpacking expression in fixedReward | 1 |
L-5 | First win tier is always empty in packFixedRewards | 1 |
L-6 | Limits in getMinimumEligibleReferralsFactorCalculation should be inclusive | 1 |
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
Instances (2):
File: src/VRFv2RNSource.sol 3: pragma solidity ^0.8.7;
File: src/staking/StakedTokenLock.sol 3: pragma solidity ^0.8.17;
claimable
function doesn't validate ticket draw is finishedhttps://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L159-L168
The function should validate that the draw associated with the ticket is already finished, as the function uses the winning ticket to run the calculation and this value will be undefined until the draw is finished and a ticket is selected.
DRAWS_PER_YEAR
assumes one draw per weekhttps://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotteryMath.sol#L24
The DRAWS_PER_YEAR
constant is fixed at 52 assuming one draw lasts one week, while the lottery can be configured with an arbitrary draw period.
fixedReward
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotterySetup.sol#L126-L127
The following calculation:
uint256 mask = uint256(type(uint16).max) << (winTier * 16); uint256 extracted = (nonJackpotFixedRewards & mask) >> (winTier * 16);
Can be simplified using a single shift as:
uint256 extracted = (nonJackpotFixedRewards >> (winTier * 16)) & type(uint16).max;
packFixedRewards
The rewards for the first win tier (tier 0) are always 0, meaning the lowest 16 bits of the packed rewards word are always 0 wasting this space. Consider offsetting the tier by 1 (store tier 1 in lowest 16 bits, tier 2 in next 16 bits, and so on) to take advantage of this space. This change also allows the protocol to support a max selection size of 17 instead of 16.
getMinimumEligibleReferralsFactorCalculation
should be inclusivehttps://github.com/code-423n4/2023-03-wenwin/blob/main/src/ReferralSystem.sol#L111-L130
function getMinimumEligibleReferralsFactorCalculation(uint256 totalTicketsSoldPrevDraw) internal view virtual returns (uint256 minimumEligible) { if (totalTicketsSoldPrevDraw < 10_000) { // 1% return totalTicketsSoldPrevDraw.getPercentage(PercentageMath.ONE_PERCENT); } if (totalTicketsSoldPrevDraw < 100_000) { // 0.75% return totalTicketsSoldPrevDraw.getPercentage(PercentageMath.ONE_PERCENT * 75 / 100); } if (totalTicketsSoldPrevDraw < 1_000_000) { // 0.5% return totalTicketsSoldPrevDraw.getPercentage(PercentageMath.ONE_PERCENT * 50 / 100); } return 5000; }
The protocol documentation specifies that the total ticket sold limits should be inclusive during the calculation of the minimum referral eligibility. However, the different conditions in the if statements use a strict inequality to define the bounds to calculate the eligibility factor.
#0 - thereksfour
2023-03-12T08:08:33Z
3L 1 INFO DOWN: 5 LOW
#1 - c4-judge
2023-03-12T08:08:38Z
thereksfour marked the issue as grade-a
#2 - c4-sponsor
2023-03-14T10:10:22Z
0xluckydev marked the issue as sponsor confirmed
#3 - 0xluckydev
2023-03-14T10:10:29Z
High quality
#4 - thereksfour
2023-03-18T04:22:02Z
8L 1 INFO A
#5 - c4-judge
2023-03-18T04:37:51Z
thereksfour marked the issue as selected for report
#6 - thereksfour
2023-03-22T04:55:25Z
Of all QA issues for this warden, #486, #431, #428, #423, and #413 are considered valid L issues, and in the QA report, L-02, L-03, and L-06 are considered valid L issues, and in addition, NC-06 would be considered an INFO because although event issues would be considered as NC, I consider that privileged functions that do not emit events should be considered as INFO. L-01,L-04,L-05 are considered to be NC. What L and INFO mean
According to https://docs.code4rena.com/awarding/judging-criteria/severity-categorization, QA issues will be categorized as low-risk and NC, on the basis of which I will further categorize the low-risk into L and INFO to scoring where L = 10 points and INFO = 5 points. In the low-risk issues, they will be considered INFO due to lower likelihood, higher permission requirements, lack of clear description, etc. I will use an example to illustrate the difference between L and INFO. In the C4 criteria, the 0 address check would be considered low-risk, but if it occurs in constructor, I will treat it as INFO because it is less likelihood.
🌟 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
12.7206 USDC - $12.72
claimable
function visibilityThe claimable
function in the Lottery contract is declared as external
, but it is used within the contract as an external call. Consider changing the visibility to public
in order to save gas by executing a local jump within the contract instead of an external call.
claimWinningTicket
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L266
unclaimedCount[ticketsInfo[ticketId].drawId][ticketsInfo[ticketId].combination]--;
Consider storing ticketsInfo[ticketId]
as a local variable to prevent a second sload.
claimPerDraw
functionhttps://github.com/code-423n4/2023-03-wenwin/blob/main/src/ReferralSystem.sol#L139 https://github.com/code-423n4/2023-03-wenwin/blob/main/src/ReferralSystem.sol#L145
The claimPerDraw
function reads the unclaimedTickets
storage variable to calculate and claim pending rewards. The function first reads it in line 139 to calculate the referrer side and then needlessly reloads it in line 145 to calculate the player side.
_transferOwnership
in StakedTokenLock constructorhttps://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/StakedTokenLock.sol#L17
The base contract Ownable2Step
constructor will already give ownership to the caller. The call on line 17 can be safely removed to save gas.
balanceOf
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/StakedTokenLock.sol#L14
The depositedBalance
storage variable in the StakedTokenLock
contract is superfluous since this value can be directly obtained from the staking token balanceOf
function. Consider removing this variable and its usages to save gas.
#0 - c4-judge
2023-03-12T13:45:00Z
thereksfour marked the issue as grade-b
#1 - rand0c0des
2023-03-14T11:28:24Z
High quality gas report
#2 - c4-sponsor
2023-03-14T11:28:31Z
rand0c0des marked the issue as sponsor confirmed