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: 27/93
Findings: 1
Award: $169.80
π 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
169.7989 USDC - $169.80
During the audit, 9 low and 5 non-critical issues were found.
β | Title | Risk Rating | Instance Count |
---|---|---|---|
L-1 | Change ticket type from uint256 to uint120 | Low | 2 |
L-2 | If refferer == address (0), make refferer = msg.sender | Low | 1 |
L-3 | Misleading comment | Low | 1 |
L-4 | Validate nonJackpotFixedRewards | Low | 1 |
L-5 | Use SafeCast Library | Low | 8 |
L-6 | Validate minimum values | Low | 2 |
L-7 | Change constant names | Low | 2 |
L-8 | Make contracts more universal | Low | 3 |
L-9 | Consider returning zero value instead of reverting | Low | 1 |
NC-1 | Order of Functions | Non-Critical | 5 |
NC-2 | Order of Layout | Non-Critical | 2 |
NC-3 | Prevent zero transfers | Non-Critical | 5 |
NC-4 | Unused named return variables | Non-Critical | 8 |
NC-5 | Missing leading underscores | Non-Critical | 15 |
In the modifier requireValidTicket(uint256 ticket)
and in the function isValidTicket()
, ticket has uint256 type, though in the registerTicket()
function, ticket has uint128 type. And even here and here it is written that "Ticket is represented as uint120 packed ticket".
In the modifier requireValidTicket(uint256 ticket)
and in the function isValidTicket()
change ticket type to uint120.
refferer`` == address (0), make
refferer`` = msg.senderThe protocol wants to maximize efficiency for participants, so maybe if the referrer is set to address(0) in the buyTickets() function, then make msg.sender the referrer. The user could buy a lot of tickets (and pass refferal requirements) but find out the opportunity to set themself a refferer lately - it will be sad to know about missed βbonusβ ).
if (refferer == address (0)) { refferer = msg.sender; }
The comment is "0 - staking reward, 1 - frontend reward", but it should be "0 - frontend reward, 1 - staking reward".
Change the comment.
nonJackpotFixedRewards
It can be validated that rewards are in ascending order, for example: reward for (3/7) < reward for (4/7) < reward for (5/7) and so on.
Downcasting from uint256/int256 in Solidity does not revert on overflow. This can easily result in undesired exploitation or bugs, since developers usually assume that overflows raise errors. SafeCast restores this intuition by reverting the transaction when such an operation overflows.
Although it is impossible that somebody will buy so many tickets ( >= uint128.max + 1), it is still better to do not use unsafe type casting to avoid even theoretical truncation and prevent loss of user funds:
unclaimedTickets[currentDraw][referrer].referrerTicketCount += uint128(numberOfTickets);
unclaimedTickets[currentDraw][player].playerTicketCount += uint128(numberOfTickets);
Other instances:
uint16 reward = uint16(rewards[winTier] / divisor);
currentNetProfit += int256(unclaimedJackpotTickets * winAmount[drawId][selectionSize]);
newProfit = oldProfit + int256(ticketsSalesToPot);
newProfit -= int256(expectedRewardsOut);
excessPotInt -= int256(fixedJackpotSize);
excessPot = excessPotInt > 0 ? uint256(excessPotInt) : 0;
It is better to use safe casting library.
It can be validated that MAX_MAX_FAILED_ATTEMPTS
and MAX_REQUEST_DELAY
are not set to too small values.
Change to:
if (_maxFailedAttempts > MAX_MAX_FAILED_ATTEMPTS || _maxFailedAttempts < MIN_MAX_FAILED_ATTEMPTS) { revert MaxFailedAttemptsTooBigOrTooSmall(); } if (_maxRequestDelay > MAX_REQUEST_DELAY || _maxRequestDelay < MIN_REQUEST_DELAY) { revert MaxRequestDelayTooBigOrTooSmall();
Here one constant has double "MAX" but other - single:
uint256 private constant MAX_MAX_FAILED_ATTEMPTS = 10; uint256 private constant MAX_REQUEST_DELAY = 5 hours;
Change to:
uint256 private constant MAX_FAILED_ATTEMPTS = 10; uint256 private constant MAX_REQUEST_DELAY = 5 hours;
or
uint256 private constant MAX_MAX_FAILED_ATTEMPTS = 10; uint256 private constant MAX_MAX_REQUEST_DELAY = 5 hours;
Protocol team wants to make different lotteries with different parameters without changing contracts, so it is better to change some constants to immutables and add parameters.
uint256 public constant override INITIAL_SUPPLY = 1_000_000_000e18;
- change to immutable and set in constructorconstructor() ERC20("Wenwin Lottery", "LOT") {
- name_
and symbol_
should be parametersnativeToken = new LotteryToken();
- add the parametersConsider returning zero value instead of reverting in the function claimWinningTicket. So, the user can pass as parameter (in the function claimWinningTickets) all their ticketIds β user just will receive rewards without checking which tickets are winning and which are not.
Change:
revert NothingToClaim(ticketId);
to:
return 0;
According to Style Guide, ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier.
Functions should be grouped according to their visibility and ordered:
Reorder functions where possible.
According to Order of Layout, inside each contract, library or interface, use the following order:
Modifiers should be placed before constructor:
Events should be placed before functions:
Place events and modifiers before functions.
It can be checked that amount/claimedAmount/tickets.length > 0 to avoid zero transfers and do not spend gas.
stakedToken.transferFrom(msg.sender, address(this), amount);
stakedToken.transfer(msg.sender, amount);
rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length);
rewardToken.safeTransfer(beneficiary, claimedAmount);
rewardToken.safeTransfer(msg.sender, claimedAmount);
Before the transfer check that transferredAmount > 0.
Both named return variable(s) and return statement are used.
To improve clarity use only named return variables.
For example, change:
function functionName() returns (uint id) { return x;
to
function functionName() returns (uint id) { id = x;
Internal and private constants, immutables, state variables and functions should have a leading underscore.
Constants:
Immutables:
State variables:
uint256 private claimedStakingRewardAtTicketId;
mapping(address => uint256) private frontendDueTicketSales;
mapping(uint128 => mapping(uint120 => uint256)) private unclaimedCount;
Functions:
function packFixedRewards(uint256[] memory rewards) private view returns (uint256 packed) {
function registerTicket(
function receiveRandomNumber(uint256 randomNumber) internal override onlyWhenExecutingDraw {
function drawRewardSize(uint128 drawId, uint8 winTier) private view returns (uint256 rewardSize) {
function dueTicketsSoldAndReset(address beneficiary) private returns (uint256 dueTickets) {
function claimWinningTicket(uint256 ticketId) private onlyTicketOwner(ticketId) returns (uint256 claimedAmount) {
function returnUnclaimedJackpotToThePot() private {
function requireFinishedDraw(uint128 drawId) internal view override {
function mintNativeTokens(address mintTo, uint256 amount) internal override {
Add leading underscores where needed.
#0 - thereksfour
2023-03-12T07:57:22Z
4 L 4 INFO 6 NC DOWN 1 L
#1 - c4-judge
2023-03-12T07:57:26Z
thereksfour marked the issue as grade-b
#2 - c4-judge
2023-03-12T10:54:08Z
thereksfour marked the issue as grade-a
#3 - c4-sponsor
2023-03-14T09:57:43Z
0xluckydev marked the issue as sponsor confirmed
#4 - thereksfour
2023-03-18T04:16:02Z
5 L 4 INFO A