2023-03-wenwin-findings's Issues
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);
}
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.
Gas Optimizations
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.
QA Report
See the markdown file with the details of this report here.
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
- The
owner
keeps fundedLINK
in the lottery sufficiently low, but not enough to let the request fail, and waits until the jackpot is big enough - After that, he stops funding the
Lottery
, and he buys a ticket for the next draw - The
Lottery
callsrequestRandomNumber
and fails due to lack of funds; users callretry
, which failsmaxFailedAttempts
times for the same reason - The
owner
swaps the source with his contract that provides arandomNumber
that matches his ticket, thus, he wins the jackpot
Hacked owner
- The
owner
had its private key compromised, but he doesn't know about it - The
attacker
keeps track of fundedLINK
and waits for an opportunity. He buys a ticket when funds are low and the jackpot is high - The
Lottery
callsrequestRandomNumber
and fails due to lack of funds; users callretry
, which failsmaxFailedAttempts
times for the same reason attacker
swaps the source with his contract that provides arandomNumber
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:
- Remove the
swapSource
function entirely - Add a governance mechanism to let users approve the new
source
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.
Missing checks for frontend address in buyTickets
Lines of code
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);
}
Gas Optimizations
See the markdown file with the details of this report here.
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.
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.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
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.
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
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
Gas Optimizations
See the markdown file with the details of this report here.
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.
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.
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
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:
- C4 Certified Contributor Terms and Conditions,
- C4 Code of Professional Conduct, and
- the disclosure guidelines below.
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:
- any sponsor staff or sponsor contractors who are also participating as wardens
- any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
- 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)
- any other case where someone might reasonably infer a possible conflict of interest.
Gas Optimizations
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.
Gas Optimizations
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
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])
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.
Gas Optimizations
See the markdown file with the details of this report here.
QA Report
See the markdown file with the details of this report here.
Gas Optimizations
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
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.
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.
Initial Token Distribution
Lines of code
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.
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
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.
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.
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
reconstructTicket(randomnumber, 7, 35) will tend to shy away from 34, 33, 32, 31, 30, 29
Lines of code
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));
}
}
Use safeMint instead of mint for ERC721
Lines of code
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
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.
Gas Optimizations
See the markdown file with the details of this report here.
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google β€οΈ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.