GithubHelp home page GithubHelp logo

2023-03-wenwin-findings's Introduction

Wenwin Contest

Unless otherwise discussed, this repo will be made public after contest completion, sponsor review, judging, and two-week issue mitigation window.

Contributors to this repo: prior to report publication, please review the Agreements & Disclosures issue.


Contest findings are submitted to this repo

Sponsors have three critical tasks in the contest process:

  1. Weigh in on severity.
  2. Respond to issues.
  3. Share your mitigation of findings.

Let's walk through each of these.

High and Medium Risk Issues

Please note: because wardens submit issues without seeing each other's submissions, there will always be findings that are duplicates. For all issues labeled 3 (High Risk) or 2 (Medium Risk), these have been pre-sorted for you so that there is only one primary issue open per unique finding. All duplicates have been labeled duplicate, linked to a primary issue, and closed.

Weigh in on severity

Judges have the ultimate discretion in determining severity of issues, as well as whether/how issues are considered duplicates. However, sponsor input is a significant criteria.

For a detailed breakdown of severity criteria and how to estimate risk, please refer to the judging criteria in our documentation.

If you disagree with a finding's severity, leave the severity label intact and add the label disagree with severity, along with a comment indicating your opinion for the judges to review. You may also add questions for the judge in the comments.

Respond to issues

Label each open/primary High or Medium risk finding as one of these:

  • sponsor confirmed, meaning: "Yes, this is a problem and we intend to fix it."
  • sponsor disputed, meaning either: "We cannot duplicate this issue" or "We disagree that this is an issue at all."
  • sponsor acknowledged, meaning: "Yes, technically the issue is correct, but we are not going to resolve it for xyz reasons."

(Note: please don't use sponsor disputed for a finding if you think it should be considered of lower or higher severity. Instead, use the label disagree with severity and add comments to recommend a different severity level -- and include your reasoning.)

Add any necessary comments explaining your rationale for your evaluation of the issue.

Note that when the repo is public, after all issues are mitigated, wardens will read these comments; they may also be included in your C4 audit report.

QA and Gas Reports

For low and non-critical findings (AKA QA), as well as gas optimizations: wardens are required to submit these as bulk listings of issues and recommendations. They may only submit a single, compiled report in each category:

  • QA reports should include all low severity and non-critical findings, along with a summary statement.
  • Gas reports should include all gas optimization recommendations, along with a summary statement.

For QA and Gas reports, sponsors are not required to weigh in on severity or risk level. We ask that sponsors:

  • Leave a comment for the judge on any reports you consider to be particularly high quality. (These reports will be awarded on a curve.)
  • Add the sponsor disputed label to any reports that you think should be completely disregarded by the judge, i.e. the report contains no valid findings at all.

Once labelling is complete

When you have finished labelling findings, drop the C4 team a note in your private Discord backroom channel and let us know you've completed the sponsor review process. At this point, we will pass the repo over to the judge to review your feedback while you work on mitigations.

Share your mitigation of findings

Note: this section does not need to be completed in order to finalize judging. You can continue work on mitigations while the judge finalizes their decisions and even beyond that. Ultimately we won't publish the final audit report until you give us the OK.

For each finding you have confirmed, you will want to mitigate the issue before the contest report is made public.

As you undertake that process, we request that you take the following steps:

  1. Within a repo in your own GitHub organization, create a pull request for each finding.
  2. Link the PR to the issue that it resolves within your contest findings repo.

This will allow for complete transparency in showing the work of mitigating the issues found in the contest. If the issue in question has duplicates, please link to your PR from the open/primary issue.

2023-03-wenwin-findings's People

Contributors

code423n4 avatar c4-judge avatar kartoonjoy avatar itsmetechjay avatar

Stargazers

小米 avatar

Watchers

Ashok avatar  avatar

Forkers

csjaybit

2023-03-wenwin-findings's Issues

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Every ticket sale can be sandwiched to steal staking rewards

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/Staking.sol#L48

Vulnerability details

Impact

The staking rewards are calculated on a rewards / tokenStaked basis. This means that time is not taken into account during the computation of rewards and allows a sandwich attack every time a ticket sale happens, opening the possibility to the attacker to earn a fraction of the rewards without having exposure to the LOT token.

Proof of Concept

The timeline will look something like this:

1.- An attacker sees on the mempool a transaction (t1) that is gonna buy a considerable amount of tickets.
2.- The attacker frontruns t1 and buy LOT tokens and stake them.
3.- The attacker let t1 to be executed
4.- The attacker withdraw its stake and sell the LOT tokens.

The attacker only needs to calculate the amount of tokens that will yield him a profit. Must take into account the potential gain from the rewards minus the fees for buying and selling LOT. Because the attacker front-run and then back-run the sale, he is not exposed to any prince change of the LOT token.

Tools Used

Manual Review

Recommended Mitigation Steps

After staking add a small lock period.

Users can refer themselves to get rewards

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L114
https://github.com/code-423n4/2023-03-wenwin/blob/main/src/ReferralSystem.sol#L76-L82

Vulnerability details

Impact

Users can refer themselves to get reward.

Proof of Concept

The referrer address in the buyTickets() function parameters are not checked in any way,users can refer themselves.
Afterwards, users can call the function claimReferralReward() to get native tokens.

   function buyTickets(
        uint128[] calldata drawIds,
        uint120[] calldata tickets,
        address frontend,
        address referrer
    )
        external
        override
        requireJackpotInitialized
        returns (uint256[] memory ticketIds)
    {
        if (drawIds.length != tickets.length) {
            revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);
        }

    function claimReferralReward(uint128[] memory drawIds) external override returns (uint256 claimedReward) {
        for (uint256 counter = 0; counter < drawIds.length; ++counter) {
            claimedReward += claimPerDraw(drawIds[counter]);
        }

        mintNativeTokens(msg.sender, claimedReward);
    }

Tools Used

Vscode

Recommended Mitigation Steps

No maximum length for tickets array per call to Lottery.sol.buyTickets function and Possible DOS (out-of-gas) on loop.

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L112
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L125
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L192
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Ticket.sol#L26

Vulnerability details

Impact

One type of DoS attack in smart contracts is externally manipulate a map or array loop. This situation is generally due to the fact that the mapping or array loop can be manipulated externally by others, and since the length of the mapping or array loop is not limited, resulting in massive consumption of Ether and Gas, finally, the smart contract is temporarily or permanently inoperable.

DoS is where an attacker deliberately overloads or crashes a system or network, making it unavailable to users. The goal of a DoS attack is to prevent legitimate users from accessing the targeted system or network, either by overwhelming it with excessive traffic or by exploiting vulnerabilities in the system's design.

By consuming the resources of the contract, the attacker can make the user temporarily quit the non-operable contract, or even permanently quit the contract.

Lottery.sol.buyTickets function gets an array of tickets as input with no maximum limit on the length of the array. So malicious users can use a large array of tickets on Lottery.sol.buyTickets function and cause DoS or User can use a large array of tickets on this function and cause DoS in for loop on the Lottery.sol#L125 due to reaching the block gas limit.

Proof of Concept

As you can see at Lottery.sol#L112, buyTickets function gets an array of tickets as input and iterates over this array by for loop and call mint function to mint NFT ticket to the caller.

The problem here is that this is a batch minting function and there is not any limit on the maximum number of indexes on the uint120[] calldata tickets.

function buyTickets(

uint128[] calldata drawIds,

uint120[] calldata tickets,

address frontend,

address referrer

)

external

override

requireJackpotInitialized

returns (uint256[] memory ticketIds)

{

if (drawIds.length != tickets.length) {

revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);

}

ticketIds = new uint256;

for (uint256 i = 0; i < drawIds.length; ++i) {

ticketIds[i] = registerTicket(drawIds[i], tickets[i], frontend, referrer);

}

referralRegisterTickets(currentDraw, referrer, msg.sender, tickets.length);

frontendDueTicketSales[frontend] += tickets.length;

rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length);

}

If we face a malicious user, she/he can use an array with a large number of indexes to make the call to the buyTickets function at line Lottery.sol#L110, by consuming the resources of the contract, the attacker can make the user temporarily quit the non-operable contract, or even permanently quit the contract.

But if a normal user wants to use the buyTickets function at line Lottery.sol#L110 with an array with a large number of indexes, this function can potentially DoS due to reaching the block gas limit and there is possible to get an out of gas issue while iterating the for loop in this function.

For example in the pancakeswap Lottery smart contract, we use below requirements,

https://github.com/pancakeswap/lottery-contract/blob/d2ebe90700f842bd62280e0ecc7bb95c2475f2c4/contracts/Lottery.sol#L491
https://github.com/pancakeswap/lottery-contract/blob/d2ebe90700f842bd62280e0ecc7bb95c2475f2c4/contracts/Lottery.sol#L515
https://github.com/pancakeswap/lottery-contract/blob/d2ebe90700f842bd62280e0ecc7bb95c2475f2c4/contracts/LotteryNFT.sol#L192

Tools Used

Manually

Recommended Mitigation Steps

Set the maximum number of tickets per call to the Lottery.sol.buyTickets function.

QA Report

See the markdown file with the details of this report here.

Medium severity issues found in LotteryToken, VRFv2RNSource, StakedTokenLock, and Staking Contracts

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/LotteryToken.sol#L22
https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/VRFv2RNSource.sol#L33
https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/StakedTokenLock.sol#L50
https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/Staking.sol#L108

Vulnerability details

LotteryToken contract

Summary

The LotteryToken contract is an ERC20 token contract with a fixed initial supply of 1 billion tokens that can be increased by minting new tokens after each draw. The inflation rates are defined for each year. The contract has a constructor that initializes the contract and mints the initial supply of tokens to the contract owner. The contract appears to be well-written and follows best practices.

Authorization check for mint function:

Finding: The mint function only allows the owner to mint new tokens, but there is no authorization check to ensure that only the owner can call this function. This could lead to the unauthorized minting of tokens by malicious actors.
Recommendation: Add an authorization check to the mint function to ensure that only the owner can call it.

No limit on the maximum supply:

Finding: The contract does not set a maximum limit on the total supply of tokens that can be minted. This could lead to excessive inflation and a devaluation of the token over time.
Recommendation: Set a maximum limit on the total supply of tokens to ensure the long-term sustainability of the token.

VRFv2RNSource contract

Summary

The VRFv2RNSource contract is an implementation of the IVRFv2RNSource interface that uses Chainlink's VRFV2WrapperConsumerBase contract as a wrapper for requesting randomness from Chainlink's VRF. The contract takes a callback gas limit and the number of required confirmations as constructor arguments. The contract appears to be well-written and follows best practices.

Missing Input Validation for Callback Parameters

The function fulfillRandomWords receives an array of random numbers randomWords which is not being validated for its contents. This could potentially cause issues if an attacker were to supply an array with an unexpected data type, which could result in unexpected behavior or even exploit the contract.
Recommendation: It is recommended to add input validation in the fulfillRandomWords function. Specifically, validate the contents before using them. Consider throwing an error or returning a default value if the inputs are invalid.

StakedTokenLock contract

Summary

The StakedTokenLock contract is a simple implementation of a staked token lock contract. It allows users to deposit their staked tokens, which will be locked for a specific period of time, and receive rewards in the form of a separate token. The contract supports deposit, withdrawal, and reward-claiming functionalities.

Findings

No access control for getReward function

Finding: The getReward function has no access control, meaning any address can call this function and claim rewards.
Recommendation: Add an access control mechanism to the getReward function to ensure only authorized addresses can claim rewards.

Staking contract

Summary

The Staking contract allows users to stake tokens in exchange for rewards tokens. The rewards are distributed based on the user's balance and the total staked amount. The rewards distribution is tied to a Lottery contract that determines the rewards to be distributed. The contract implements the IStaking interface and uses SafeERC20 to ensure secure token transfers.

Findings

Potential reentrancy vulnerability

Finding: The contract uses the _updateReward function to update the reward information before any token transfer. This can potentially lead to a reentrancy attack if a malicious user calls a function that triggers a transfer during the reward update.
Recommendation: Implement OpenZeppelin's reentrancy guard in the contract.

QA Report

See the markdown file with the details of this report here.

Missing checks for frontend address, users can purchase tickets at a 10% discount

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L113

Vulnerability details

Impact

Users can purchase tickets at a 10% discount.

Proof of Concept

We know that 10% of ticket sales will be allocated to frontend. When users call the function buyTickets() to purchase tickets ,they can specify the frontend address.

 function buyTickets(
        uint128[] calldata drawIds,
        uint120[] calldata tickets,
        address frontend,
        address referrer
    )
        external
        override
        requireJackpotInitialized
        returns (uint256[] memory ticketIds)
    {
        if (drawIds.length != tickets.length) {
            revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);
        }

There is no check of the frontend address and users can specify their own address.After that users call the function claimRewards() to get frontend rewards.In the end, users purchased tickets at a 10% discount.

    function claimRewards(LotteryRewardType rewardType) external override returns (uint256 claimedAmount) {
        address beneficiary = (rewardType == LotteryRewardType.FRONTEND) ? msg.sender : stakingRewardRecipient;
        claimedAmount = LotteryMath.calculateRewards(ticketPrice, dueTicketsSoldAndReset(beneficiary), rewardType);

        emit ClaimedRewards(beneficiary, claimedAmount, rewardType);
        rewardToken.safeTransfer(beneficiary, claimedAmount);
    }

Tools Used

Vscode

Recommended Mitigation Steps

Whitelist the frontend address

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

User can always buy tickets at a discount

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L110

Vulnerability details

Impact

There's no validation for the frontend address when calling buyTickets. This means any user can set themselves to be the frontend and get a FRONTEND_REWARD percent discount all the time when buying tickets.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


import "./LotteryTestBase.sol";
import "../src/Lottery.sol";
import "./TestToken.sol";
import "test/TestHelpers.sol";

contract MoreLotteryTest is LotteryTestBase {
    address public constant OWNER = address(0x111);
    
    address public constant USER = address(123);
    
    function buyTicketOnDiscount(uint128 draw, uint120 ticket, address referrer) internal returns (uint256 ticketId) {
        uint128[] memory drawIds = new uint128[](1);
        drawIds[0] = draw;
        uint120[] memory tickets = new uint120[](1);
        tickets[0] = ticket;

        uint256[] memory ticketIds = lottery.buyTickets(drawIds, tickets, USER, referrer);
        return ticketIds.length > 0 ? ticketIds[0] : 0;
    }

    function testBuyTicket() public {
        uint128 currentDraw = lottery.currentDraw();
        uint256 initialBalance = rewardToken.balanceOf(address(lottery));

        vm.startPrank(USER);
        rewardToken.mint(5 ether);
        rewardToken.approve(address(lottery), 10 ether);
        console.log(rewardToken.balanceOf(address(lottery)));
        buyTicketOnDiscount(currentDraw, uint120(0x0F), address(0));
        lottery.claimRewards(LotteryRewardType.FRONTEND);
        console.log(rewardToken.balanceOf(address(lottery)));
        assertEq(rewardToken.balanceOf(address(lottery)), initialBalance + TICKET_PRICE);
    }

}

Tools Used

forge test

Recommended Mitigation Steps

Establish some kind of whitelist for valid frontends to avoid this problem.

A malicious or hacked owner can easily steal the jackpot

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/RNSourceController.sol#L89-L104

Vulnerability details

Lottery uses Chainlink to get a randomNumber, that will be used to extract the numbers required to win the jackpot.

There is a function swapSource that can be called by the owner, which can be used to swap Chainlink with anything else.

This can happen only if the Chainlink request fails, and the following is true:

  • maxFailedAttempts requests are made, and all fail
  • Each request can be made only after maxRequestDelay time

There is an upper limit, but no lower limit to maxFailedAttempts and maxRequestDelay.

File: src/RNSourceController.sol

 89:     function swapSource(IRNSource newSource) external override onlyOwner {
 90:         if (address(newSource) == address(0)) {
 91:             revert RNSourceZeroAddress();
 92:         }
 93:         bool notEnoughRetryInvocations = failedSequentialAttempts < maxFailedAttempts;
 94:         bool notEnoughTimeReachingMaxFailedAttempts = block.timestamp < maxFailedAttemptsReachedAt +  axRequestDelay;
 95:         if (notEnoughRetryInvocations || notEnoughTimeReachingMaxFailedAttempts) {
 96:             revert NotEnoughFailedAttempts();
 97:         }
 98:         source = newSource;
 99:         failedSequentialAttempts = 0;
100:         maxFailedAttemptsReachedAt = 0;
101: 
102:         emit SourceSet(newSource);
103:         requestRandomNumberFromSource();
104:     }

The documentation says:

It's important to understand that the deployer cannot change the randomness source other than in the unlikely scenario in which the Chainlink oracle fails repeatedly, as per the specifications above. Additionally, the predefined timeframe and the number of allowed retry attempts are immutable and cannot be changed after the contracts have been deployed.

This is not really true, as what it takes to let the request fail is simply not having enough LINK, as the Lottery uses the direct funding method with Chainlink.

Impact

The owner or a hacked owner can steal all the funds by forcibly winning the jackpot due to the possibility of changing the randomNumber source.

Proof of Concept

Malicious owner

  1. The owner keeps funded LINK in the lottery sufficiently low, but not enough to let the request fail, and waits until the jackpot is big enough
  2. After that, he stops funding the Lottery, and he buys a ticket for the next draw
  3. The Lottery calls requestRandomNumber and fails due to lack of funds; users call retry, which fails maxFailedAttempts times for the same reason
  4. The owner swaps the source with his contract that provides a randomNumber that matches his ticket, thus, he wins the jackpot

Hacked owner

  1. The owner had its private key compromised, but he doesn't know about it
  2. The attacker keeps track of funded LINK and waits for an opportunity. He buys a ticket when funds are low and the jackpot is high
  3. The Lottery calls requestRandomNumber and fails due to lack of funds; users call retry, which fails maxFailedAttempts times for the same reason
  4. attacker swaps the source with his contract that provides a randomNumber that matches his ticket, thus, he wins the jackpot

Tools Used

Manual review

Recommended Mitigation Steps

There are a few ways to solve this issue.

Consider one of these:

  1. Remove the swapSource function entirely
  2. Add a governance mechanism to let users approve the new source

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a 👍 emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

Use safeMint instead of mint for ERC721

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Ticket.sol#L26

Vulnerability details

Impact

Use safeMint instead of mint for ERC721.
In the Ticket.sol#L26, msg.sender used as an address to mint ticket. However, if msg.sender is a contract address that does not support ERC721, the NFT can be frozen in the contract.

Proof of Concept

In the Ticket.sol#L26, msg.sender used as an address to mint ticket. However, if msg.sender is a contract address that does not support ERC721, the NFT can be frozen in the contract.

As per the documentation of EIP-721:
A wallet/broker/auction application MUST implement the wallet interface if it will accept safe transfers.
Ref: https://eips.ethereum.org/EIPS/eip-721
As per the documentation of ERC721.sol by Openzeppelin
Ref: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L274-L285

Tools Used

Manually

Recommended Mitigation Steps

Use safeMint instead of mint to check received address support for ERC721 implementation.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L262

QA Report

See the markdown file with the details of this report here.

Result of transfer / transferFrom not checked

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/staking/StakedTokenLock.sol#L34
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/staking/StakedTokenLock.sol#L55
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/staking/StakedTokenLock.sol#L47

Vulnerability details

Impact

The transfer and transferFrom functions in the smart contract do not have proper error handling. This can lead to unexpected behaviors such as loss of funds or incorrect account balances.

Proof of Concept

src/staking/StakedTokenLock.sol

34: stakedToken.transferFrom(msg.sender, address(this), amount);

47: stakedToken.transfer(msg.sender, amount);

55: rewardsToken.transfer(owner(), rewardsToken.balanceOf(address(this)));

Tools Used

Manual Review

Recommended Mitigation Steps

To mitigate this vulnerability, proper error handling should be added to the transfer and transferFrom functions. This can be achieved by checking the return value of the transfer and transferFrom functions and throwing an error if the return value indicates a failure.

Referrers who meet the minimum referral requirement may also not be rewarded

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/ReferralSystem.sol#L100

Vulnerability details

Impact

Referrers who meet the minimum referral requirement may also not be rewarded due to rounding error.

Proof of Concept

Referrers who meet the minimum referral requirement will be eligible for the individual referrer allocation.
Referrer reward per draw for one ticket is calculated according to this formula referrerRewardForDraw / totalTicketsForReferrersPerCurrentDraw.The referrerRewardForDraw[] is specified at the time of initialization and cannot be modified. If there are a lot of tickets for referrers current draw and it is greater than referrer reward for draw, the referrer reward per draw for one ticket may be 0. And the clamed rewards claimedReward = referrerRewardPerDrawForOneTicket[drawId] * _unclaimedTickets.referrerTicketCount; will be 0.
Referrers who meet the minimum referral requirement may also not be rewarded.

 function referralDrawFinalize(uint128 drawFinalized, uint256 ticketsSoldDuringDraw) internal {
        // if no tickets sold there is no incentives, so no rewards to be set
        if (ticketsSoldDuringDraw == 0) {
            return;
        }

        minimumEligibleReferrals[drawFinalized + 1] =
            getMinimumEligibleReferralsFactorCalculation(ticketsSoldDuringDraw);

        uint256 referrerRewardForDraw = referrerRewardsPerDraw(drawFinalized);
        uint256 totalTicketsForReferrersPerCurrentDraw = totalTicketsForReferrersPerDraw[drawFinalized];
        if (totalTicketsForReferrersPerCurrentDraw > 0) {
            referrerRewardPerDrawForOneTicket[drawFinalized] =
                referrerRewardForDraw / totalTicketsForReferrersPerCurrentDraw;
        }


Tools Used

Vscode

Recommended Mitigation Steps

Allow smart contract to buy tickets on behalf of users or _safeMint() should be used rather than _mint() wherever possible

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L126
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L192
https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Ticket.sol#L26

Vulnerability details

Impact

Big projects in defi have the ability to get integrated into another project, for example, the deposit function in the AAVE project has the ability on behalf of others. That means users can use with AAVE by Intermediary smart contract.

But in a wenwin and Lottery.sol contract, there are two scenarios:
First, you want to allow the Intermediary smart contracts to get integrated into your project, so many users can participate in your project using an Intermediary smart contract owned by another EOA. If you follow this scenario, based on Lottery.sol.buyTickets function logic, all tickets are registered to the Intermediary smart contract and not to the users.

Second, you don’t want to allow smart contracts to call Lottery.sol.buyTickets function. So, you should first check the caller and allow calls to the Lottery.sol.buyTickets function only if the caller is EAO.

Proof of Concept

If you want to allow Intermediary smart contracts to get integrated into your project, you need to add another input in Lottery.sol.buyTickets function and it is onBehalfOf address or to address. Using this feature, Intermediary smart contracts that are integrated into your project can get money from the user and mint NFT tickets for the user without any problem and risk of losing money and ticket for the user.

But based on the current logic of Lottery.sol.buyTickets function and registerTicket function at line Lottery.sol#L192, tickets are registered to the msg.sender ( Intermediary smart contracts ) and not to the users. So if an Intermediary smart contract becomes malicious, the user will lose money.

But if you don’t want to allow smart contracts to make calls to the Lottery.sol.buyTickets function, you need to add the modifier to this function to prevent calls from smart contracts. Because you are using _mint(to, ticketId) function in the Ticket.sol contract, Ticket.sol#L26, to mint ticket to the users. _mint() is discouraged in favor of _safeMint() which ensures that the recipient is either an EOA or implements IERC721Receiver. Both open OpenZeppelin and solmate have versions of this function so that NFTs aren't lost if they're minted to contracts that cannot transfer them back out.

Another related example is the pancakeswap Lottery contract. In the pancakeswap Lottery contract, In any case, the use of the Intermediary smart contract is not allowed, this contract is using modifier notContract on the BuyLottoTicket.

https://code4rena.com/reports/2022-04-backed/#l-06-_safemint-should-be-used-rather-than-_mint-wherever-possible

Tools Used

Manually

Recommended Mitigation Steps

Use the notContract modifier on the Lottery.sol.buyTickets function or add onBehalfOf input on this function and mint tickets for the onBehalfOf address.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

SWC-101 Integer Overflow Lottery.sol buyTickets() testBuyInvalidTicket()

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L110-L131

Vulnerability details

Impact

# Using Foundry:

Arithmetic overflow has caused the owner account to reduce to 0 and the attack user account to increase by the balance transferred from the owner.
# Vulnerable Code> Lottery.sol:

    function buyTickets(
        uint128[] calldata drawIds,
        uint120[] calldata tickets,
        address frontend,
        address referrer
    )
        external
        override
        requireJackpotInitialized
        returns (uint256[] memory ticketIds)
    {
        if (drawIds.length != tickets.length) {
            revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);
        }
        ticketIds = new uint256[](tickets.length);
        for (uint256 i = 0; i < drawIds.length; ++i) {
            ticketIds[i] = registerTicket(drawIds[i], tickets[i], frontend, referrer);
        }
        referralRegisterTickets(currentDraw, referrer, msg.sender, tickets.length);
        frontendDueTicketSales[frontend] += tickets.length;
        rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length);
    }

Proof of Concept


# Command Line:
forge test -vvv  --match-path "test/Lottery.t.sol" --match-test "testBuyInvalidTicket"

# Foundry> Payload> Lottery.t.sol:
function testBuyInvalidTicket() public {
        uint128 currentDraw = lottery.currentDraw();

        vm.startPrank(USER);
        rewardToken.mint(TICKET_PRICE+1);
        rewardToken.approve(address(lottery), TICKET_PRICE);

        vm.expectRevert(InvalidTicket.selector);
        buyTicket(currentDraw, uint120(0x0E)+1, address(0));

        // Cannot buy 10000000111
        vm.expectRevert(InvalidTicket.selector);
        buyTicket(currentDraw, uint120(0x407)+1, address(0));

        // Can buy 1000000111
        buyTicket(currentDraw, uint120(0x207)+1, address(0));
    }
# Log:
username@name-MacBook-Pro 2023-03-wenwin % forge test -vvv  --match-path "test/Lottery.t.sol" --match-test "testBuyInvalidTicket"
[⠢] Compiling...
No files changed, compilation skipped

Running 1 test for test/Lottery.t.sol:LotteryTest
[FAIL. Reason: Call did not revert as expected] testBuyInvalidTicket() (gas: 274887)
Traces:
  [274887] LotteryTest::testBuyInvalidTicket() 
    ├─ [2504] Lottery::currentDraw() [staticcall]
    │   └─ ← 0
    ├─ [0] VM::startPrank(0x000000000000000000000000000000000000007B) 
    │   └─ ← ()
    ├─ [29444] TestToken::mint(5000000000000000001) 
    │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x000000000000000000000000000000000000007B, value: 5000000000000000001)
    │   └─ ← ()
    ├─ [24628] TestToken::approve(Lottery: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 5000000000000000000) 
    │   ├─ emit Approval(owner: 0x000000000000000000000000000000000000007B, spender: Lottery: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 5000000000000000000)
    │   └─ ← true
    ├─ [0] VM::expectRevert(InvalidTicket()) 
    │   └─ ← ()
    ├─ [182634] Lottery::buyTickets([0], [15], 0x00000000000000000000000000000000000001bc, 0x0000000000000000000000000000000000000000) 
    │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x000000000000000000000000000000000000007B, tokenId: 0)
    │   ├─ emit NewTicket(currentDraw: 0, ticketId: 0, drawId: 0, user: 0x000000000000000000000000000000000000007B, combination: 15, frontend: 0x00000000000000000000000000000000000001bc, referrer: 0x0000000000000000000000000000000000000000)
    │   ├─ [8498] TestToken::transferFrom(0x000000000000000000000000000000000000007B, Lottery: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 5000000000000000000) 
    │   │   ├─ emit Approval(owner: 0x000000000000000000000000000000000000007B, spender: Lottery: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 0)
    │   │   ├─ emit Transfer(from: 0x000000000000000000000000000000000000007B, to: Lottery: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 5000000000000000000)
    │   │   └─ ← true
    │   └─ ← [0]
    └─ ← "Call did not revert as expected"

Test result: FAILED. 0 passed; 1 failed; finished in 5.03ms

Failing tests:
Encountered 1 failing test in test/Lottery.t.sol:LotteryTest
[FAIL. Reason: Call did not revert as expected] testBuyInvalidTicket() (gas: 274887)

Encountered a total of 1 failing tests, 0 tests succeeded

Tools Used

Foundry + VS Code

Recommended Mitigation Steps

It is recommended to use vetted safe math libraries for arithmetic operations consistently throughout the smart contract system.

Initial Token Distribution

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/LotteryToken.sol#L19

Vulnerability details

Impact

LotteryToken is the native token of Wenwin Lottery. It can be staked (stakers receive a portion of ticket sales) and referral rewards.

considering that this token is the native token of this project, the security of the tokens and economic design is very important for this token.

But based on the smart contract of LotteryToken, in the constructor function of LotteryToken at LotteryToken.sol#L19, we see that the initial supply of this token 1_000_000_000e18 is minted for the deployer or EOA.

Proof of Concept

But based on the smart contract of LotteryToken, in the constructor function of LotteryToken at LotteryToken.sol#L19, we see that the initial supply of this token 1_000_000_000e18 is minted for the deployer or EOA.

constructor() ERC20("Wenwin Lottery", "LOT") {

owner = msg.sender;

_mint(msg.sender, INITIAL_SUPPLY);

}

The hacked owner or malicious owner can abuse this number of minted Tokens and endanger the users' money or the future of the project. This could be a centralization risk as the deployer can distribute Lottery Tokens without obtaining the consensus of the community.

If you want to split the tokens based on any document ( I could not find any documentation on this ), you can mint the initial supply to the Governance – DAO contract and do this split process with better transparency and more security.

Tools Used

Manually

Recommended Mitigation Steps

recommend transparency by providing a breakdown of the intended initial token distribution in a public location. also, recommend the team try to restrict the access of the corresponding private key.

Missing checks for frontend address in buyTickets

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/Lottery.sol#L129

Vulnerability details

Impact

Missing checks for the value of the frontend address in the buyTickets function allows for setting the frontend end address to the zero address, this could potentially lead to the loss of rewards, as there will be no way to recover the rewards

Proof of Concept

N/A

Tools Used

Manual Review

Recommended Mitigation Steps

Add checks to make sure frontend address is valid

    function buyTickets(
        uint128[] calldata drawIds,
        uint120[] calldata tickets,
        address frontend,
        address referrer
    )
        external
        override
        requireJackpotInitialized
        returns (uint256[] memory ticketIds)
    {
        if (drawIds.length != tickets.length) {
            revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);
        }
        ticketIds = new uint256[](tickets.length);
        for (uint256 i = 0; i < drawIds.length; ++i) {
            ticketIds[i] = registerTicket(drawIds[i], tickets[i], frontend, referrer);
        }
        referralRegisterTickets(currentDraw, referrer, msg.sender, tickets.length);
        require(frontend != address(0), "Frontend address cannot be 0");
        frontendDueTicketSales[frontend] += tickets.length;
        rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length);
    }

Total loss of staking rewards when the `rewardToken` has less than 18 decimals

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/staking/Staking.sol#L58

Vulnerability details

Impact

Users that stake their tokens get more or fewer rewards than reserved, based on the number of rewardToken decimals, potentially zero if the rewardToken has less than 18 decimals, leading to a total loss of rewards.

Proof of Concept

Staking token uses 18 decimals as default, but Lottery can choose any token as a reward token, including tokens that don't have 18 decimals.

For example, the rewardToken could be USDC, which has 6 decimals.

There is a function rewardPerToken() that is used to calculate how many staking tokens should be earned, which is assumed to be in base 1e18:

File: src/staking/Staking.sol

55:         uint256 unclaimedRewards =
56:             LotteryMath.calculateRewards(lottery.ticketPrice(), ticketsSoldSinceUpdate, LotteryRewardType.STAKING);
57: 
58:         return rewardPerTokenStored + (unclaimedRewards * 1e18 / _totalSupply);

However, if USDC is used as a reward this assumption is wrong. So this function will return a lot fewer tokens than deserved.

In the case of USDC there would be a difference of 12 decimals, which would lead to a loss of ~99.99%

Finally, this value is used in the earned function:

File: src/staking/Staking.sol

62:     function earned(address account) public view override returns (uint256 _earned) {
63:         return balanceOf(account) * (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18 + rewards[account];
64:     }

And here, due to a rounding error, (rewardPerToken() - userRewardPerTokenPaid[account]) / 1e18 will always round to zero if the decimals are not at least 18, leading to a total loss of funds, because earned will always return 0.

Tools Used

Manual review

Recommended Mitigation Steps

Modify rewardPerToken() to always normalize in base 1e18:

-         return rewardPerTokenStored + (unclaimedRewards * 1e18 / _totalSupply);
+         return rewardPerTokenStored + (unclaimedRewards * 10 ** (36 - lottery.rewardToken().decimals()) / _totalSupply);

QA Report

See the markdown file with the details of this report here.

User can always buy tickets at a discount

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L110

Vulnerability details

Impact

There's no validation for the frontend address when calling buyTickets. This means any user can set themselves to be the frontend and get a FRONTEND_REWARD percent discount all the time when buying tickets.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


import "./LotteryTestBase.sol";
import "../src/Lottery.sol";
import "./TestToken.sol";
import "test/TestHelpers.sol";

contract MoreLotteryTest is LotteryTestBase {
    address public constant OWNER = address(0x111);
    
    address public constant USER = address(123);
    
    function buyTicketOnDiscount(uint128 draw, uint120 ticket, address referrer) internal returns (uint256 ticketId) {
        uint128[] memory drawIds = new uint128[](1);
        drawIds[0] = draw;
        uint120[] memory tickets = new uint120[](1);
        tickets[0] = ticket;

        uint256[] memory ticketIds = lottery.buyTickets(drawIds, tickets, USER, referrer);
        return ticketIds.length > 0 ? ticketIds[0] : 0;
    }

    function testBuyTicket() public {
        uint128 currentDraw = lottery.currentDraw();
        uint256 initialBalance = rewardToken.balanceOf(address(lottery));

        vm.startPrank(USER);
        rewardToken.mint(5 ether);
        rewardToken.approve(address(lottery), 10 ether);
        console.log(rewardToken.balanceOf(address(lottery)));
        buyTicketOnDiscount(currentDraw, uint120(0x0F), address(0));
        lottery.claimRewards(LotteryRewardType.FRONTEND);
        console.log(rewardToken.balanceOf(address(lottery)));
        assertEq(rewardToken.balanceOf(address(lottery)), initialBalance + TICKET_PRICE);
    }

}

Tools Used

forge test

Recommended Mitigation Steps

Establish some kind of whitelist for valid frontends to avoid this problem.

Incorrect erc721 interface

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L70

Vulnerability details

Impact

Incorrect return values for ERC721 functions. A contract compiled with solidity > 0.4.22 interacting with these functions will fail to execute them, as the return value is missing.

Proof of Concept

File: src/Lottery.sol

70:         if (ownerOf(ticketId) != msg.sender) {

Exploit Scenario:

contract Token{
function ownerOf(uint256 _tokenId) external view returns (bool);
//...
}
Token.ownerOf does not return an address like ERC721 expects. Bob deploys the token. Alice creates a contract that interacts with it but assumes a correct ERC721 interface implementation. Alice's contract is unable to interact with Bob's contract.

Tools Used

VS Code, Slither

Recommended Mitigation Steps

Set the appropriate return values and vtypes for the defined ERC721 functions.

QA Report

See the markdown file with the details of this report here.

Missing checks for drawIds,users may not have a chance to win

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L111

Vulnerability details

Impact

If the draw id is very large ,users may not have a chance to win.

Proof of Concept

By calling the function buyTickets() users will purchase tickets,where the draw is conducted weekly, every Wednesday.The user can arbitrarily specify the draw id in the parameter.Every Wednesday the protocol calculates the winning ticket according the currcent draw. If the draw id is very large ,users may not have a chance to win.

function buyTickets(
        uint128[] calldata drawIds,
        uint120[] calldata tickets,
        address frontend,
        address referrer
    )
        external
        override
        requireJackpotInitialized
        returns (uint256[] memory ticketIds)
    {
        if (drawIds.length != tickets.length) {
            revert DrawsAndTicketsLenMismatch(drawIds.length, tickets.length);
        }
        ticketIds = new uint256[](tickets.length);
        for (uint256 i = 0; i < drawIds.length; ++i) {
            ticketIds[i] = registerTicket(drawIds[i], tickets[i], frontend, referrer);
        }
        referralRegisterTickets(currentDraw, referrer, msg.sender, tickets.length);
        frontendDueTicketSales[frontend] += tickets.length;
        rewardToken.safeTransferFrom(msg.sender, address(this), ticketPrice * tickets.length);
    }
    function receiveRandomNumber(uint256 randomNumber) internal override onlyWhenExecutingDraw {
        uint120 _winningTicket = TicketUtils.reconstructTicket(randomNumber, selectionSize, selectionMax);
        uint128 drawFinalized = currentDraw++;
        uint256 jackpotWinners = unclaimedCount[drawFinalized][_winningTicket];

Tools Used

Vscode

Recommended Mitigation Steps

Limit the range of draw

Lost of reward token

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/Lottery.sol#L271

Vulnerability details

Impact

The current implementation of returnUnclaimedJackpotToThePot only works correctly and reclaims the profit by updating currentNetProfit when there is a jackpot winner. In the case where there isn't one and some winning tickets go unclaimed, the currentNetProfit is not calculated correctly resulting in permanent lost of funds since there's no alternative way of transferring the reward token away from the Lottery contract.

Proof of Concept

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;


import "./LotteryTestBase.sol";
import "../src/Lottery.sol";
import "./TestToken.sol";
import "test/TestHelpers.sol";

contract MoreLotteryTest is LotteryTestBase {
    address public constant OWNER = address(0x111);
    
    address public constant USER = address(123);

    function testNonJackpotWinClaimable() public {
        uint128 drawId = lottery.currentDraw();
        uint256 ticketId = initTickets(drawId, 0x8E);

        // this will give winning ticket of 0x0F so 0x8E will have 3/4
        finalizeDraw(0);

        uint8 winTier = 3;
        checkTicketWinTier(drawId, 0x8E, winTier);
        // claimWinnings(drawId, ticketId, winTier, fixedRewards[winTier]);
        int256 beforeReturn = lottery.currentNetProfit();
        console.logInt(beforeReturn);
        for (uint256 i = 0; i < 52; ++i) {
            finalizeDraw(0);
        }
        int256 afterReturn = lottery.currentNetProfit();
        console.logInt(afterReturn);
        assertFalse(beforeReturn == afterReturn);
    }

    function testJackpotWinClaimable() public {
        uint128 drawId = lottery.currentDraw();
        uint256 ticketId = initTickets(drawId, 0x0F);

        // this will give winning ticket of 0x0F so 0x8E will have 3/4
        finalizeDraw(0);

        uint8 winTier = 4;
        checkTicketWinTier(drawId, 0x0F, winTier);
        // claimWinnings(drawId, ticketId, winTier, fixedRewards[winTier]);
        int256 beforeReturn = lottery.currentNetProfit();
        console.logInt(beforeReturn);
        for (uint256 i = 0; i < 52; ++i) {
            finalizeDraw(0);
        }
        int256 afterReturn = lottery.currentNetProfit();
        console.logInt(afterReturn);
        assertFalse(beforeReturn == afterReturn);
    }

    function initTickets(uint128 drawId, uint120 numbers) private returns (uint256 ticketId) {
        vm.startPrank(USER);
        rewardToken.mint(TICKET_PRICE);
        rewardToken.approve(address(lottery), TICKET_PRICE);
        ticketId = buyTicket(drawId, numbers, address(0));
        // buy the same tickets to increase nonJackpot count
        buySameTickets(drawId, uint120(0xF0), address(0), 10);
        vm.stopPrank();
    }

    function checkTicketWinTier(uint128 drawId, uint120 ticket, uint256 expectedWinTier) private {
        uint120 winningTicket = lottery.winningTicket(drawId);
        uint256 winTier = TicketUtils.ticketWinTier(ticket, winningTicket, SELECTION_SIZE, SELECTION_MAX);
        assertEq(winTier, expectedWinTier);
    }


}

Tools Used

forge test

Recommended Mitigation Steps

Update the implementation of returnUnclaimedJackpotToThePot to account for the case where there's no jackpot winner.

Incorrect calculation of currentNetProfit

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/main/src/LotteryMath.sol#L35

Vulnerability details

Impact

During the receiveRandomNumber function the Lottery.sol contract computes currentNetProfit via LotteryMath.calculateNewProfit. This function looks like this:

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)
            // @audit does not make sense
            : calculateMultiplier(calculateExcessPot(oldProfit, fixedJackpotSize), ticketsSold, expectedPayout)
                * ticketsSold * expectedPayout;

        newProfit -= int256(expectedRewardsOut);
    }

First newProfit is increased by a percentage of the total sale. Then it must decreased by the rewards to be paid. In case no ones wins the jackpot and the excess pot is zero, newProfit is decreased by ticketsSold * expectedPayout, when the correct computation should be to decrease it by the total amount earned by the users in the corresponding drawId.

Tools Used

Manual Review

Recommended Mitigation Steps

The amount to decrease newProfit should be computed as follows:

totalAmount = multiplier * (winAmount[drawId][1] + winAmount[drawId][2] + ... winAmount[drawId][selectionSize - 1])

High severity issues found in StakedTokenLock and LotterySetup contract

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/StakedTokenLock.sol#L34
https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/StakedTokenLock.sol#L47

Vulnerability details

StakedTokenLock contract

Using unchecked transfers

Finding: The contract uses unchecked transfers, which may cause unexpected behavior if the receiving address is a malicious contract. This can lead to the loss of user funds.

The issue is present in the deposit (https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/StakedTokenLock.sol#L34) and withdraw (https://github.com/code-423n4/2023-03-wenwin/blob/940066dc3c500cf745afc9e9381e131da7f98e88/src/staking/StakedTokenLock.sol#L47) functions.

In both functions, the line that transfers tokens (stakedToken.transferFrom and stakedToken.transfer) is marked with slither-disable-next-line unchecked-transfer, which means that the transfer is not using a safe transfer function, such as safeTransferFrom or safeTransfer, which can prevent potential errors and vulnerabilities. This can lead to potential reentrancy attacks or other vulnerabilities.

Recommendation: Use the SafeERC20 library to perform transfers and avoid the risk of sending tokens to a malicious contract.

reconstructTicket(randomnumber, 7, 35) will tend to shy away from 34, 33, 32, 31, 30, 29

Lines of code

https://github.com/code-423n4/2023-03-wenwin/blob/91b89482aaedf8b8feb73c771d11c257eed997e8/src/TicketUtils.sol#L43-L76

Vulnerability details

Impact

Detailed description of the impact of this finding.
reconstructTicket(randomnumber, 7, 35) will tend to shy away from 34, 33, 32, 31, 30, 29. That means the random numbers that are selected are not uniformly distributed from [0-34]. So the game might not be fair.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

The reconstructTicket(randomnumber, 7, 35) first select 7 numbers from a given random number, and then make some adjustment in case these numbers might be duplicate.

However, the following code shows that the selection is not uniformly distributed:

for (uint256 i = 0; i < selectionSize; ++i) {
            numbers[i] = uint8(randomNumber % currentSelectionCount);
            randomNumber /= currentSelectionCount;
            currentSelectionCount--;
        }

While number 34 is possible for ONLY for first iteration, the number 0-27 is possible for every iteration. Number 33 is possible only for iteration 1 and 2; number 32 is possible only for iteration 1, 2, and 3. In summary, not all the numbers have the same probability to be chosen.

Moreover, there is a discrepency between the implemetatation and the NatSpec, which says "
Reconstructs ticket from random number. Each number is selected from appropriate 8 bits from random number.
/// In each iteration, we calculate the modulo of a random number and then shift it for 8 bits to the right.


The shifting is by ``currentSelectionCount``, and for each iteration, it does not pick the 8 bits. 


## Tools Used
Remix

## Recommended Mitigation Steps
We need to make sure the game is fair, and conform the the NatSpec

```diff
function reconstructTicket(
        uint256 randomNumber,
        uint8 selectionSize,
        uint8 selectionMax
    )
        internal
        pure
        returns (uint120 ticket)
    {
        /// Ticket must contain unique numbers, so we are using smaller selection count in each iteration
        /// It basically means that, once `x` numbers are selected our choice is smaller for `x` numbers
        uint8[] memory numbers = new uint8[](selectionSize);
        uint256 currentSelectionCount = uint256(selectionMax);

        for (uint256 i = 0; i < selectionSize; ++i) {
-            numbers[i] = uint8(randomNumber % currentSelectionCount);
+            numbers[i] =  uint8(randomNumber) % selectionMax;
-            randomNumber /= currentSelectionCount;
+            randomNumber >>= 8;
-            currentSelectionCount--;
        }

        bool[] memory selected = new bool[](selectionMax);

        for (uint256 i = 0; i < selectionSize; ++i) {
            uint8 currentNumber = numbers[i];
            // check current selection for numbers smaller than current and increase if needed
            for (uint256 j = 0; j <= currentNumber; ++j) {
                if (selected[j]) {
                    currentNumber++;
                }
            }
            selected[currentNumber] = true;
            ticket |= ((uint120(1) << currentNumber));
        }
    }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.