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: 41/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
Number | Details | Instances |
---|---|---|
1 | x += y /x -= y COSTS MORE GAS THAN x = x + y /x = x - y FOR STATE VARIABLES | 11 |
2 | CAN DECLARE VARIABLE OUTSIDE LOOP TO SAVE GAS | 2 |
3 | MULTIPLE ADDRESS/ID MAPPINGS CAN BE COMNBINED INTO A SINGLE MAPPING TO A STRUCT | 10 |
4 | AVOID CONTRACT EXISTENCE CHECKS BY USING LOW LEVEL CALLS | 1 |
5 | PUBLIC FUNCTIONS NOT CALLED BY THE CONTRACT SHOULD BE DECLARED EXTERNAL INSTEAD | 2 |
6 | USE bytes32 INSTEAD OF string WHEREVER POSSIBLE | 1 |
7 | OPTIMIZE NAMES TO SAVE GAS | 24 |
8 | INTERNAL FUNCTIONS ONLY CALLED ONCE CAN BE INLINED TO SAVE GAS | 5 |
9 | STATE VARIABLES CAN BE PACKED INTO FEWER STORAGE SLOTS | 1 |
10 | revert() STATEMENTS THAT CHECK INPUT ARGUMENTS SHOULD BE AT THE TOP OF THE FUNCTION | 1 |
11 | BEFORE SOME FUNCTIONS, WE SHOULD CHECK SOME VARIABLES FOR POSSIBLE GAS SAVE | 2 |
12 | REORDER STRUCTURE LAYOUT | 1 |
13 | CACHE STORAGE VALUES IN MEMORY TO MINIMIZE SLOADS | 12 |
14 | SHOULD USE LOCAL ARGUMENTS INSTEAD OF STATE VARIABLE | 1 |
15 | USE ASSEMBLY TO CHECK FOR address(0) | 10 |
16 | USE require INSTEAD OF assert | 2 |
17 | USE calldata INSTEAD OF memory | 3 |
x += y
/x -= y
COSTS MORE GAS THAN x = x + y
/x = x - y
FOR STATE VARIABLESUsing the addition operator instead of plus-equals saves some gas for state variables.
129: frontendDueTicketSales[frontend] += tickets.length; 173: claimedAmount += claimWinningTicket(ticketIds[i]); 275: currentNetProfit += int256(unclaimedJackpotTickets * winAmount[drawId][selectionSize]);
64: totalTicketsForReferrersPerDraw[currentDraw] += 65: unclaimedTickets[currentDraw][referrer].referrerTicketCount; 67: totalTicketsForReferrersPerDraw[currentDraw] += numberOfTickets; 69: unclaimedTickets[currentDraw][referrer].referrerTicketCount += uint128(numberOfTickets); 71: unclaimedTickets[currentDraw][player].playerTicketCount += uint128(numberOfTickets); 78: claimedReward += claimPerDraw(drawIds[counter]); 147: claimedReward += playerRewardsPerDrawForOneTicket[drawId] * _unclaimedTickets.playerTicketCount;
src\staking\StakedTokenLock.sol
30: depositedBalance += amount; 43: depositedBalance -= amount;
Declaring the stack variable outside the loop will save gas that would otherwise be spent on declaring the variable over and over again.
170: uint16 reward = uint16(rewards[winTier] / divisor);
66: uint8 currentNumber = numbers[i];
If both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key’s keccak256 hash (Gkeccak256 - 30 gas) and that calculation’s associated stack operations. Depending on the circumstances and sizes of types, can avoid a Gsset per mapping combined.
27: mapping(uint128 => mapping(uint120 => uint256)) private unclaimedCount; 36: mapping(uint128 => uint120) public override winningTicket; 37: mapping(uint128 => mapping(uint8 => uint256)) public override winAmount;
17: mapping(uint128 => mapping(address => UnclaimedTicketsData)) public override unclaimedTickets; 18: 19: mapping(uint128 => uint256) public override totalTicketsForReferrersPerDraw; 20: 21: mapping(uint128 => uint256) public override referrerRewardPerDrawForOneTicket; 22: 23: mapping(uint128 => uint256) public override playerRewardsPerDrawForOneTicket; 24: 25: mapping(uint128 => uint256) public override minimumEligibleReferrals;
19: mapping(address => uint256) public override userRewardPerTokenPaid; 20: mapping(address => uint256) public override rewards;
Before Solidity 0.8.10 the compiler inserted extra code to check for contract existence for external function calls EXTCODESIZE
(100 gas). In more recent solidity versions, the compiler will not insert these checks if the external call has a return value. Similar behavior can be achieved in earlier versions by using low-level calls, since low level calls never check for contract existence.
29: requestId = requestRandomness(callbackGasLimit, requestConfirmations, 1);
Contracts are allowed to override their parents’ functions and change the visibility from public to external and can save gas by doing so.
234: function currentRewardSize(uint8 winTier) public view override returns (uint256 rewardSize) {
120: function fixedReward(uint8 winTier) public view override returns (uint256 amount) {
bytes32
INSTEAD OF string
WHEREVER POSSIBLEUse bytes32 instead of string to save gas whenever possible. String is a dynamic data structure and therefore is more gas consuming then bytes32.
114: } catch Error(string memory reason) {
Function hashes that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted. Prioritize the most called functions and sort and rename them according to the function hashes/method IDs. For a better understanding please refer to this link
The method IDs in the Lottery.sol will be used the most. A lower method ID may be given to the most frequently used functions. This is a useful tool that can be used for the same, if required.
Sighash | Function Signature ======================== 67967759 => mintNativeTokens(address,uint256) 0b6e0961 => buyTickets(uint128[],uint120[],address,address) 2d49a7bf => executeDraw() 451b19de => unclaimedRewards(LotteryRewardType) 41f66c0c => claimRewards(LotteryRewardType) d1d58b25 => claimable(uint256) df9dafab => claimWinningTickets(uint256[]) f7f2ae99 => registerTicket(uint128,uint120,address,address) 054387ad => receiveRandomNumber(uint256) 420d7990 => currentRewardSize(uint8) e0f1985a => drawRewardSize(uint128,uint8) 47da3f0f => dueTicketsSoldAndReset(address) ad25b575 => claimWinningTicket(uint256) 77959b4b => returnUnclaimedJackpotToThePot() b7f8dae2 => requireFinishedDraw(uint128)
Not inlining costs 20 to 40 gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
160: function _baseJackpot(uint256 _initialPot) internal view returns (uint256) {
111: function getMinimumEligibleReferralsFactorCalculation(uint256 totalTicketsSoldPrevDraw) 112: internal 113: view 114: virtual 115: returns (uint256 minimumEligible) 156: function playerRewardsPerDraw(uint128 drawId) internal view returns (uint256 rewards) { 161: function referrerRewardsPerDraw(uint128 drawId) internal view returns (uint256 rewards) {
108: function _beforeTokenTransfer(address from, address to, uint256) internal override {
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables are also cheaper.
11: IRNSource public override source; 12: 13: uint256 public override failedSequentialAttempts; 14: uint256 public override maxFailedAttemptsReachedAt; 15: uint256 public override lastRequestTimestamp; 16: bool public override lastRequestFulfilled = true;
revert()
STATEMENTS THAT RESULT FROM CHECKING INPUT ARGUMENTS SHOULD BE AT THE TOP OF THE FUNCTIONBy doing these checks first, the function is able to revert before wasting a Gcoldsload (2100 gas*) in a function that may ultimately revert in the unhappy case.
170: uint16 reward = uint16(rewards[winTier] / divisor); 171: if ((rewards[winTier] % divisor) != 0) { 172: revert InvalidFixedRewardSetup(); 173: }
Before transfer, we should check for amount being 0 so the function doesnt run when its not gonna do anything.
src\staking\StakedTokenLock.sol
34: stakedToken.transferFrom(msg.sender, address(this), amount); 47: stakedToken.transfer(msg.sender, amount);
The following structs could be optimized by moving the position of certain variables in order to optimize storage.
src\interfaces\ILotterySetup.sol
63: struct LotterySetupParams { 64: /// @dev Token to be used as reward token for the lottery 65: IERC20 token; 66: /// @dev Parameters of the draw schedule for the lottery 67: LotteryDrawSchedule drawSchedule; 68: /// @dev Price to pay for playing single game (including fee) 69: uint256 ticketPrice; 70: /// @dev Count of numbers user picks for the ticket 71: uint8 selectionSize; 72: /// @dev Max number user can pick 73: uint8 selectionMax; 74: /// @dev Expected payout for one ticket, expressed in `rewardToken` 75: uint256 expectedPayout; 76: /// @dev Array of fixed rewards per each non jackpot win 77: uint256[] fixedRewards; 78: }
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
133: function executeDraw() external override whenNotExecutingDraw { 134: // slither-disable-next-line timestamp 135: if (block.timestamp < drawScheduledAt(currentDraw)) { 136: revert ExecutingDrawTooEarly(); 137: } 138: returnUnclaimedJackpotToThePot(); 139: drawExecutionInProgress = true; 140: requestRandomNumber(); 141: emit StartedExecutingDraw(currentDraw); 142: } 272: if (currentDraw >= LotteryMath.DRAWS_PER_YEAR) { 273: uint128 drawId = currentDraw - LotteryMath.DRAWS_PER_YEAR;
133: if (initialPot > 0) { 147: assert(initialPot > 0);
62: if (unclaimedTickets[currentDraw][referrer].referrerTicketCount + numberOfTickets >= minimumEligible) { 63: if (unclaimedTickets[currentDraw][referrer].referrerTicketCount < minimumEligible) { 64: totalTicketsForReferrersPerDraw[currentDraw] += 65: unclaimedTickets[currentDraw][referrer].referrerTicketCount; 66: } 67: totalTicketsForReferrersPerDraw[currentDraw] += numberOfTickets; 68: } 69: unclaimedTickets[currentDraw][referrer].referrerTicketCount += uint128(numberOfTickets);
68: uint256 failedAttempts = ++failedSequentialAttempts; 73: emit Retry(source, failedSequentialAttempts);
Using the local arguments of the function while emitting an event instead of the state variable saves roughly 122 gas per emit.
73: emit Retry(source, failedSequentialAttempts);
address(0)
Saves 6 gas per instance if using assembly to check for address(0).
42: if (address(lotterySetupParams.token) == address(0)) {
60: if (referrer != address(0)) {
78: if (address(rnSource) == address(0)) { 81: if (address(source) != address(0)) { 90: if (address(newSource) == address(0)) {
31: if (address(_lottery) == address(0)) { 34: if (address(_rewardsToken) == address(0)) { 37: if (address(_stakingToken) == address(0)) { 109: if (from != address(0)) { 113: if (to != address(0)) {
require
INSTEAD OF assert
assert() function when false, uses up all the remaining gas and reverts all the changes made.
147: assert(initialPot > 0);
99: assert((winTier <= selectionSize) && (intersection == uint256(0)));
calldata
INSTEAD OF memory
When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution.
164: function packFixedRewards(uint256[] memory rewards) private view returns (uint256 packed) {
76: function claimReferralReward(uint128[] memory drawIds) external override returns (uint256 claimedReward) {
32: function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
#0 - c4-judge
2023-03-12T14:02:47Z
thereksfour marked the issue as grade-a
#1 - c4-sponsor
2023-03-14T12:56:46Z
TutaRicky marked the issue as sponsor acknowledged