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: 62/93
Findings: 1
Award: $21.70
🌟 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
Total Low issues |
---|
Risk | Issues Details | Number |
---|---|---|
[L-01] | Avoid shadowing inherited state variables | 2 |
[L-02] | Loss of precision due to rounding | 7 |
[L-03] | Use safeMint instead of mint for ERC721 | 1 |
[L-04] | Integer overflow by unsafe casting | 6 |
[L-05] | Missing checks for address(0) | 2 |
[L-06] | Multiple Pragma used | All Contracts |
[L-07] | Use require() instead of assert() | 2 |
[L-08] | LotteryToken.sol dosen't have a transfer ownership pattern | 1 |
Total Non-Critical issues |
---|
Risk | Issues Details | Number |
---|---|---|
[NC-01] | Include return parameters in NatSpec comments | All Contracts |
[NC-02] | Non-usage of specific imports | All Contracts |
[NC-03] | Lack of event emit | 1 |
[NC-04] | Function writing does not comply with the Solidity Style Guide | All Contracts |
[NC-05] | The protocol should include NatSpec | All Contracts |
[NC-06] | Constants in comparisons should appear on the left side | 10 |
[NC-07] | Use a more recent version of solidity | 2 |
[NC-08] | Generate perfect code headers every time | All Contracts |
[NC-09] | Lock pragmas to specific compiler version | 2 |
[NC-10] | There is no need to cast a variable that is already an uint, such as uint(x) | 3 |
[NC-11] | Consider using delete rather than assigning zero to clear values | 4 |
[NC-12] | For functions, follow Solidity standard naming conventions | All Contracts |
[NC-13] | Add NatSpec Mapping comment | 17 |
[NC-14] | Constants should be defined rather than using magic numbers | 4 |
[NC-15] | Not using the named return variables anywhere in the function is confusing | 15 |
In Staking.sol
there is a local variables named name
symbol
, but there is functions and state variables in the inherited contract ( ERC20.sol
) with the same name. This use causes compilers to issue warnings, negatively affecting checking and code readability.
string memory name,
string memory symbol
Avoid using variables with the same name.
Loss of precision due to the nature of arithmetics and rounding errors.
return rewardPerTokenStored + (unclaimedRewards * 1e18 / _totalSupply);
return balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18 + rewards[account];
uint16 reward = uint16(rewards[winTier] / divisor);
referrerRewardForDraw / totalTicketsForReferrersPerCurrentDraw;
playerRewardsPerDrawForOneTicket[drawFinalized] = playerRewardForDraw / ticketsSoldDuringDraw;
return number * percentage / PERCENTAGE_BASE;
return number * int256(percentage) / int256(PERCENTAGE_BASE);
safeMint
instead of mint for ERC721Users could lost their NFTs if msg.sender
is a contract address that does not support ERC721
, the NFT can be frozen in the contract forever.
As per the documentation of EIP-721:
A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.
As per the documentation of ERC721.sol by Openzeppelin:
_mint(to, ticketId);
Use _safeMint
instead of mint
to check received address support for ERC721 implementation.
Keep in mind that the version of solidity used, despite being greater than 0.8, does not prevent integer overflows during casting, it only does so in mathematical operations.
It is necessary to safely convert between the different numeric types.
uint16 reward = uint16(rewards[winTier] / divisor);
excessPot = excessPotInt > 0 ? uint256(excessPotInt) : 0;
numbers[i] = uint8(randomNumber % currentSelectionCount);
winTier += uint8(intersection & uint120(1));
unclaimedTickets[currentDraw][referrer].referrerTicketCount += uint128(numberOfTickets);
unclaimedTickets[currentDraw][player].playerTicketCount += uint128(numberOfTickets);
Use OpenZeppelin safeCast library.
address(0)
Check of address(0)
to protect the code from (0x0000000000000000000000000000000000000000)
address problem just in case. This is best practice or instead of suggesting that they verify _address != address(0)
, you could add some good NatSpec comments explaining what is valid and what is invalid and what are the implications of accidentally using an invalid address.
authorizedConsumer = _authorizedConsumer;
stakedToken = IStaking(_stakedToken);
Add checks for address(0)
when assigning values to address state variables.
Solidity pragma versions should be exactly same in all contracts. Currently some contracts use 0.8.19
and the rest use ^0.8.17
and ^0.8.7
Use one version for all contracts.
require()
instead of assert()
Assert should not be used except for tests, require should be used
Prior to Solidity 0.8.0, pressing a confirm consumes the remainder of the process's available gas instead of returning it, as request()/revert()
did.
The big difference between the two is that the assert()
function when false, uses up all the remaining gas and reverts all the changes made.
Meanwhile, a require()
statement when false, also reverts back all the changes made to the contract but does refund all the remaining gas fees we offered to pay. This is the most common Solidity function used by developers for debugging and error handling.
Assertion should be avoided even after solidity version 0.8.0, because its documentation states "The Assert function generates an error of type Panic(uint256)
. Code that works properly should never Panic, even on invalid external input. If this happens, you need to fix it in your contract. there's a mistake".
assert(initialPot > 0);
assert((winTier <= selectionSize) && (intersection == uint256(0)));
Use require()
instead of assert()
A transfer ownership pattern is the best practice in case the owner's private keys are lost.
Use OpenZeppelin Ownable to make the contracts more robust in the future.
If Return parameters are declared, you must prefix them with /// @return
.
Some code analysis programs do analysis by reading NatSpec details, if they can't see the @return
tag, they do incomplete analysis.
Include @return
parameters in NatSpec comments
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly.
Use specific imports syntax per solidity docs recommendation.
The below methods do not emit an event when the state changes, something that it's very important for dApps and users.
function claimWinningTickets(uint256[] calldata ticketIds) external override returns (uint256 claimedAmount) { uint256 totalTickets = ticketIds.length; for (uint256 i = 0; i < totalTickets; ++i) { claimedAmount += claimWinningTicket(ticketIds[i]); } rewardToken.safeTransfer(msg.sender, claimedAmount); }
Emit event.
Solidity Style Guide
Ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
Functions should be grouped according to their visibility and ordered:
constructor()
receive()
fallback()
external / public / internal / private
view / pure
Follow Solidity Style Guide.
It is recommended that Solidity contracts are fully annotated using NatSpec, it is clearly stated in the Solidity official documentation.
In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
Some code analysis programs do analysis by reading NatSpec details, if they can't see the tags (@param, @dev, @return)
, they do incomplete analysis.
Include NatSpec
comments in the codebase.
Constants in comparisons should appear on the left side, doing so will prevent typo bug.
if (lotterySetupParams.selectionSize == 0) {
if (claimedAmount == 0) {
if (lotterySetupParams.selectionSize == 0) {
if (initialPot == 0) {
} else if (winTier == 0 || winTier > selectionSize) {
if (_rewardsToReferrersPerDraw.length == 0) {
if (_rewardsToReferrersPerDraw[i] == 0) {
if (ticketsSoldDuringDraw == 0) {
if (_totalSupply == 0) {
if (amount == 0) {
if (amount == 0) {
Constants should appear on the left side:
if (0 == lotterySetupParams.selectionSize) {
For security, it is best practice to use the latest Solidity version.
pragma solidity ^0.8.17;
pragma solidity ^0.8.7;
Old versions of Solidity are used (^0.8.7)
, (^0.8.17)
, newer version can be used (0.8.19)
.
I recommend using header for Solidity code layout and readability
/*////////////////////////////////////////////////////////////// TESTING 123 //////////////////////////////////////////////////////////////*/
Pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case with contracts in a library or EthPM package. Otherwise, the developer would need to manually update the pragma in order to compile locally.
pragma solidity ^0.8.17;
pragma solidity ^0.8.7;
Ethereum Smart Contract Best Practices: Lock pragmas to specific compiler version.
uint(x)
There is no need to cast a variable that is already an uint, such as uint(0), 0 is also uint.
return (ticketSize == selectionSize) && (ticket == uint256(0));
if (lotterySetupParams.ticketPrice == uint256(0)) {
return (ticketSize == selectionSize) && (ticket == uint256(0));
assert((winTier <= selectionSize) && (intersection == uint256(0)));
Use directly variable:
return (ticketSize == selectionSize) && (ticket == 0);
delete
rather than assigning zero to clear valuesThe delete
keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.
frontendDueTicketSales[beneficiary] = 0;
unclaimedTickets[drawId][msg.sender].referrerTicketCount = 0;
unclaimedTickets[drawId][msg.sender].playerTicketCount = 0;
rewards[msg.sender] = 0;
Use the delete
keyword.
The protocol don't follow solidity standard naming convention.
Ref: https://docs.soliditylang.org/en/v0.8.17/style-guide.html#naming-conventions
Follow solidity standard naming convention.
Add NatSpec comments describing mapping keys and values.
mapping(address => uint256) public override rewards;
mapping(address => uint256) private frontendDueTicketSales;
mapping(uint128 => mapping(uint120 => uint256)) private unclaimedCount;
mapping(uint128 => uint120) public override winningTicket;
mapping(uint128 => mapping(uint8 => uint256)) public override winAmount;
mapping(uint128 => uint256) public override ticketsSold;
mapping(uint128 => mapping(address => UnclaimedTicketsData)) public override unclaimedTickets;
mapping(uint128 => uint256) public override totalTicketsForReferrersPerDraw;
mapping(uint128 => uint256) public override referrerRewardPerDrawForOneTicket;
mapping(uint128 => uint256) public override playerRewardsPerDrawForOneTicket;
mapping(uint128 => uint256) public override minimumEligibleReferrals;
mapping(address => uint256) public override userRewardPerTokenPaid;
mapping(address => uint256) public override rewards;
mapping(uint256 => ITicket.TicketInfo) public override ticketsInfo;
/// @dev address(user) => uint256(rewards) mapping(address => uint256) public override rewards;
Constants should be defined rather than using magic numbers.
lotterySetupParams.selectionSize > 16 || lotterySetupParams.selectionSize >= lotterySetupParams.selectionMax
uint256 mask = uint256(type(uint16).max) << (winTier * 16);
uint256 extracted = (nonJackpotFixedRewards & mask) >> (winTier * 16);
packed |= uint256(reward) << (winTier * 16);
Define a constant.
Not using the named return variables anywhere in the function is confusing.
function currentRewardSize(uint8 winTier) public view override returns (uint256 rewardSize) {
function fixedReward(uint8 winTier) public view override returns (uint256 amount) {
function drawScheduledAt(uint128 drawId) public view override returns (uint256 time) {
function ticketRegistrationDeadline(uint128 drawId) public view override returns (uint256 time) {
function getPercentage(uint256 number, uint256 percentage) internal pure returns (uint256 result) {
function getPercentageInt(int256 number, uint256 percentage) internal pure returns (int256 result) {
returns (uint256 minimumEligible)
function playerRewardsPerDraw(uint128 drawId) internal view returns (uint256 rewards) {
function referrerRewardsPerDraw(uint128 drawId) internal view returns (uint256 rewards) {
function rewardPerToken() public view override returns (uint256 _rewardPerToken) {
function earned(address account) public view override returns (uint256 _earned) {
function mint(address to, uint128 drawId, uint120 combination) internal returns (uint256 ticketId) {
returns (bool isValid)
returns (uint120 ticket)
returns (uint8 winTier)
Consider changing the variable to be an unnamed one.
#0 - thereksfour
2023-03-12T08:39:33Z
1 L 3 INFO
#1 - c4-judge
2023-03-12T08:39:38Z
thereksfour marked the issue as grade-b
#2 - c4-judge
2023-03-12T11:03:48Z
thereksfour marked the issue as grade-a
#3 - c4-sponsor
2023-03-14T10:10:46Z
0xluckydev requested judge review
#4 - 0xluckydev
2023-03-14T10:11:03Z
Duplicate of #438
#5 - thereksfour
2023-03-14T14:19:53Z
This should be the same warden submission, for some reason, #438 is not linked to the reported warden
#6 - thereksfour
2023-03-18T04:25:36Z
1 L 3 INFO
B
#7 - c4-judge
2023-03-18T04:25:41Z
thereksfour marked the issue as grade-b