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: 48/93
Findings: 2
Award: $34.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
21.7018 USDC - $21.70
The LotterySetup contract includes a function called packFixedRewards
, which takes an argument called rewards
. The rewards
argument is an array of type uint256
.
When the function calculates the reward, it divides rewards[winTier]
by divisor
and downcasts the result to uint16
.
File: src\LotterySetup.sol function packFixedRewards(uint256[] memory rewards) private view returns (uint256 packed) { ... uint16 reward = uint16(rewards[winTier] / divisor); ... }
Recommended Mitigation Steps:
Consider using OpenZeppelin’s SafeCast library to prevent unexpected overflows when casting from uint256.
Since this library was developed to use on uint256 but in the function's body we have uin256 multiplication which potentially can overflow. Consider creating a helper contract with functions that have input validation or function params that cant revert with overflow after multiplication eg
function getPercentage(uint128 number, uint128 percentage) internal pure returns (uint256 result) { return number * percentage / PERCENTAGE_BASE; }
_safeMint()
should be used rather than _mint()
The use of _mint()
function is discouraged and it is recommended to use _safeMint()
function instead. _safeMint()
ensures that the recipient is either an EOA or implements the IERC721Receiver
interface, making it a more secure option for minting new tokens.
File: src\Ticket.sol function mint(address to, uint128 drawId, uint120 combination) internal returns (uint256 ticketId) { ticketId = nextTicketId++; ticketsInfo[ticketId] = TicketInfo(drawId, combination, false); _mint(to, ticketId); }
address(0x0)
when assigning values to address
state variablesAlthough this check is present in other contracts.
File: src\staking\StakedTokenLock.sol IStaking public immutable override stakedToken; IERC20 public immutable override rewardsToken; ... constructor(address _stakedToken, uint256 _depositDeadline, uint256 _lockDuration) { ... stakedToken = IStaking(_stakedToken); rewardsToken = stakedToken.rewardsToken(); ... }
File: src\RNSourceBase.sol address internal immutable authorizedConsumer; ... constructor(address _authorizedConsumer) { authorizedConsumer = _authorizedConsumer; }
IReferralSystemDynamic.sol
does not seem to be used anywhere, as there are no imports or contracts that inherit from it.
It is generally not recommended to use the latest version of Solidity unless it is necessary, because new versions may contain undiscovered bugs or other issues that could affect the functionality or security of your smart contract. Instead, it is recommended to use a stable and well-tested version of Solidity that is known to be reliable and secure.
#0 - thereksfour
2023-03-12T07:59:02Z
1 L 2 INFO 3 NC
#1 - c4-judge
2023-03-12T07:59:13Z
thereksfour marked the issue as grade-b
#2 - c4-sponsor
2023-03-14T09:59:28Z
0xluckydev marked the issue as sponsor disputed
#3 - 0xluckydev
2023-03-14T09:59:44Z
Irrelevant except the first one
#4 - thereksfour
2023-03-17T12:22:11Z
B
🌟 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
Issue | Instances | |
---|---|---|
[G-01] | Use constants instead of type(uintx).max | 1 |
[G-02] | Using storage instead of memory for structs/arrays saves gas | 2 |
[G-03] | Using calldata in function args saves gas | 1 |
[G-04] | Use nested if and, avoid multiple check combinations | 2 |
[G-05] | Functions guaranteed to revert_ when called by normal users can be marked payable | 6 |
[G-06] | Setting the constructor to payable | 10 |
[G-07] | x += y (x -= y) costs more gas than x = x + y (x = x - y) for state variables | 2 |
[G-08] | Add unchecked {} for subtractions where the operands cannot underflow because of a previous require or if statement | 1 |
[G-09] | internal functions only called once can be inlined to save gas | 3 |
Using type(uint16).max and similar operations consumes more gas during both the distribution process and for each transaction compared to using constants.
File: src\LotterySetup.sol function fixedReward(uint8 winTier) public view override returns (uint256 amount) { ... uint256 mask = uint256(type(uint16).max) << (winTier * 16); ... }
storage
instead of memory
for structs/arrays
saves gasWhen fetching data from a storage location, assigning the data to a memory
variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD
rather than a cheap stack read. Instead of declaring the variable with the memory
keyword, declaring the variable with the storage
keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory
variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory
, or if the array/struct is being read from another memory
array/struct.
File: src\ReferralSystem.sol function claimPerDraw(uint128 drawId) private returns (uint256 claimedReward) { ... UnclaimedTicketsData memory _unclaimedTickets = unclaimedTickets[drawId][msg.sender]; ... }
File: src\Lottery.sol function claimable(uint256 ticketId) external view override returns (uint256 claimableAmount, uint8 winTier) { TicketInfo memory ticketInfo = ticketsInfo[ticketId]; ... }
calldata
in function args saves gasWhen a function argument is declared with the calldata
keyword, the function can access the argument's data directly from the transaction's input data, which is already stored in the contract's memory.
File: src\ReferralSystem.sol function claimReferralReward(uint128[] memory drawIds) external override returns (uint256 claimedReward)
if and
, avoid multiple check combinationsUsing nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
File: src\staking\StakedTokenLock.sol function withdraw(uint256 amount) external override onlyOwner { ... if (block.timestamp > depositDeadline && block.timestamp < depositDeadline + lockDuration) ... }
File: src\staking\StakedTokenLock.sol function withdraw(uint256 amount) external override onlyOwner { ... if (block.timestamp > depositDeadline && block.timestamp < depositDeadline + lockDuration) { ... }
payable
If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
File: src\staking\StakedTokenLock.sol function deposit(uint256 amount) external override onlyOwner function withdraw(uint256 amount) external override onlyOwner
File: src\RNSourceController.sol function initSource(IRNSource rnSource) external override onlyOwner function swapSource(IRNSource newSource) external override onlyOwner
payable
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0
and saves 13 gas
on deployment with no security risks.
File: src/LotteryToken.sol constructor() ERC20("Wenwin Lottery", "LOT") File: src/VRFv2RNSource.sol constructor(address _authorizedConsumer, address _linkAddress, address _wrapperAddress, uint16 _requestConfirmations, uint32 _callbackGasLimit) File: src/staking/StakedTokenLock.sol constructor(address _stakedToken, uint256 _depositDeadline, uint256 _lockDuration) File: src/staking/Staking.sol constructor(ILottery _lottery, IERC20 _rewardsToken, IERC20 _stakingToken, string memory name, string memory symbol) ERC20(name, symbol) File: src/LotterySetup.sol constructor(LotterySetupParams memory lotterySetupParams) File: src/Lottery.sol constructor(LotterySetupParams memory lotterySetupParams,uint256 playerRewardFirstDraw,uint256 playerRewardDecreasePerDraw,uint256[] memory rewardsToReferrersPerDraw,uint256 maxRNFailedAttempts,uint256 maxRNRequestDelay) File: src/Ticket.sol constructor() ERC721("Wenwin Lottery Ticket", "WLT") File: src/RNSourceBase.sol constructor(address _authorizedConsumer) File: src/RNSourceController.sol constructor(uint256 _maxFailedAttempts, uint256 _maxRequestDelay) File: src/ReferralSystem.sol constructor(uint256 _playerRewardFirstDraw,uint256 _playerRewardDecreasePerDraw, uint256[] memory _rewardsToReferrersPerDraw)
x += y (x -= y)
costs more gas than x = x + y (x = x - y)
for state variablesFile: src\staking\StakedTokenLock.sol function deposit(uint256 amount) external override onlyOwner { ... depositedBalance += amount; ... } function withdraw(uint256 amount) external override onlyOwner { ... depositedBalance -= amount; ... }
unchecked {}
for subtractions where the operands cannot underflow because of a previous require
or if
statementThis will stop the check for overflow and underflow so it will save gas.
function returnUnclaimedJackpotToThePot() private { if (currentDraw >= LotteryMath.DRAWS_PER_YEAR) { uint128 drawId = currentDraw - LotteryMath.DRAWS_PER_YEAR; ... } }
internal
functions only called once can be inlined to save gasFile: src\Lottery.sol function requireFinishedDraw(uint128 drawId) internal view override function mintNativeTokens(address mintTo, uint256 amount) internal override
File: src\LotterySetup.sol function _baseJackpot(uint256 _initialPot) internal view returns (uint256)
#0 - c4-judge
2023-03-12T13:43:10Z
thereksfour marked the issue as grade-a
#1 - c4-judge
2023-03-12T13:47:21Z
thereksfour marked the issue as grade-b
#2 - c4-judge
2023-03-12T13:59:23Z
thereksfour marked the issue as grade-a
#3 - c4-judge
2023-03-12T14:04:54Z
thereksfour marked the issue as grade-b
#4 - rand0c0des
2023-03-14T11:25:15Z
I will just give an example of [G-02] It is cheaper to copy to memory as Struct is packed to one storage slot. If we used storage pointer, it would have been reading from same slot multiple times.
#5 - c4-sponsor
2023-03-14T11:25:20Z
rand0c0des marked the issue as sponsor disputed
#6 - thereksfour
2023-03-17T12:21:12Z
B