Platform: Code4rena
Start Date: 30/10/2023
Pot Size: $49,250 USDC
Total HM: 14
Participants: 243
Period: 14 days
Judge: 0xsomeone
Id: 302
League: ETH
Rank: 47/243
Findings: 2
Award: $189.17
š Selected for report: 0
š Solo Findings: 0
š Selected for report: The_Kakers
Also found by: 00xSEV, 0x3b, Arabadzhiev, DeFiHackLabs, Fulum, Madalad, MrPotatoMagic, SpicyMeatball, Tadev, ZanyBonzy, ZdravkoHr, alexfilippov314, audityourcontracts, cheatc0d3, devival, dy, evmboi32, immeas, lsaudit, mrudenko, oakcobalt, oualidpro, pipidu83, r0ck3tz, rishabh, rotcivegaf, tpiliposian, xAriextz
175.1934 USDC - $175.19
requestRandomWords()
function in RandomizerRNG contract shouldn't be payable and should be internal or private.The RandomizerRNG includes the following 2 functions :
function requestRandomWords(uint256 tokenid, uint256 _ethRequired) public payable { require(msg.sender == gencore); uint256 requestId = arrngController.requestRandomWords{value: _ethRequired}(1, (address(this))); tokenToRequest[tokenid] = requestId; requestToToken[requestId] = tokenid; } function calculateTokenHash(uint256 _collectionID, uint256 _mintIndex, uint256 _saltfun_o) public { require(msg.sender == gencore); tokenIdToCollection[_mintIndex] = _collectionID; requestRandomWords(_mintIndex, ethRequired); }
Given the documentation and the implementation of NextGenCore contract, there is only one valid path :
NextGenCore calls calculateTokenHash()
function without sending eth. NextGenCore or any other NextGen contract doesn't make any direct call to requestRandomWords()
function. Currently, requestRandomWords()
function also requires that msg.sender == gencore
(just like calculateTokenHash()
function , which is redondant. requestRandomWords()
function should be a private function (or internal, at least) while removing the unnecessary check for msg.sender.
calculateTokenHash()
function internally calls requestRandomWords()
function, without sending ETH. There is actually no way to call requestRandomWords()
function while sending ETH, this is why payable
keyword should be removed. The final call to arrngController.requestRandomWords()
function will use ETH balance of the Randomizer contract to send _ethRequired
ETH value.
That being said, I propose to modify these functions as follows :
function calculateTokenHash(uint256 _collectionID, uint256 _mintIndex, uint256 _saltfun_o) external { require(msg.sender == gencore); // ideally, use a custom error to save gas tokenIdToCollection[_mintIndex] = _collectionID; requestRandomWords(_mintIndex, ethRequired); } function requestRandomWords(uint256 tokenid, uint256 _ethRequired) private { uint256 requestId = arrngController.requestRandomWords{value: _ethRequired}(1, (address(this))); tokenToRequest[tokenid] = requestId; requestToToken[requestId] = tokenid; }
reservedMinTokensIndex
value is greater than reservedMaxTokensIndex
value in NextGenCore contract storage, which is not supposed to happen.reservedMinTokensIndex
and reservedMaxTokensIndex
are both members of the collectionAdditonalDataStructure
struct. They are supposed to bound all available tokens for a collection.
In the following scenario:
createCollection()
functionsetCollectionData()
, passing 0 as value for _collectionTotalSupply
variable. The result will be :collectionAdditionalData[_collectionID].reservedMinTokensIndex = (_collectionID * 10000000000); collectionAdditionalData[_collectionID].reservedMaxTokensIndex = (_collectionID * 10000000000) + 0 - 1; reservedMinTokensIndex = reservedMaxTokensIndex + 1
This situation could be avoided by adding a check that _collectionTotalSupply
is strictly greater than 0.
setCollectionData()
function in NextGenCore contract and modify collectionTotalSupply
value in the second one.Documentation clearly says : "After its initial call, the setCollectionData() function can be called to update the artists eth address, the max purchases during public minting and the final supply time."
Nevertheless, if you firstly call setCollectionData()
function with a value of 0 for _collectionTotalSupply
, you can make another call to set collectionTotalSupply
value.
To respect the specification, I would recommend to do the same thing as in [Lā02]: adding a check at the beginning of setCollectionData()
function to make sure that _collectionTotalSupply
input is strictly greater than 0.
mint()
function in NextGenMinterContractDuring the public mint period (phase == 2), a call to the mint()
function will trigger a check that the number of tokens the user requested to mint is not too much. In order to do that, the function needs to compare:
_numberOfTokens
) + the number of tokens already minted by the usergencore.viewMaxAllowance(col)
, which returns maxCollectionPurchases
This means only the following check is required :
require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");
The check executed just before is clearly redondant and not sufficient, as it only checks that _numberOfTokens
is lower than maxCollectionPurchases
:
require(_numberOfTokens <= gencore.viewMaxAllowance(col), "Change no of tokens");
I suggest to keep only the 2nd check, and delete the first check. This would also save gas.
setCollectionData()
function in NextGenCore contract to make sure _maxCollectionPurchases < _collectionTotalSupply
Currently, there is no sanity check for all uint256 values that are passed to setCollectionData()
function (except _collectionTotalSupply <= 10000000000
).
This means it could be possible to create a collection with _maxCollectionPurchases < _collectionTotalSupply
, which is not supposed to happen.
It could be a good idea to add the following check (or ideally, with a custom error):
require(_maxCollectionPurchases <= _collectionTotalSupply, "aberrant max value");
setCollectionPhases()
function in NextGenMinterContract to make sure that _allowlistStartTime < _allowlistEndTime
and _publicStartTime < _publicEndTime
.setCollectionPhases()
function can be called with wrong values for _allowlistStartTime
, _allowlistEndTime
, _publicStartTime
and _publicEndTime
. This could lead to a collection not working correctly, with mint()
, burnToMint()
and burnOrSwapExternalToMint()
functions reverting, and getPrice()
function not working correctly.
It could be a good idea to a check to make sure _allowlistStartTime > _allowlistEndTime
and _publicStartTime > _publicEndTime
.
proposePrimaryAddressesAndPercentages()
and proposeSecondaryAddressesAndPercentages
functions for percentagesAll percentage system uses 100 as the total value than can be splitted.
Both functions use this check :
require( _add1Percentage + _add2Percentage + _add3Percentage == collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage, "Check %" );
This means all percentages could only be integer value, as artistPercentage
is an integer between 0 and 100. It is not possible to choose, for example, _add1Percentage
representing 6.5%. This could be improved to allow more granularity in the way funds are split between artist's addresses and the team's addresses. Using 1000 or 10 000 as a base for the percentage system would allow this.
As precised by the sponsor in the discord channel of the contest: "artists will need to sign the collection before even minting starts, this will be an agreement between team and artist".
But currently, minting period could start although artist didn't sign the collection, there is no check preventing this from happening.
Therefore, I suggest to add a getter function in NextGenCore contract:
function hasArtistSigned(uint256 _collectionID) public view returns (bool) { return artistSigned[_collectionID]; }
and a check in setCollectionCosts()
function in NextGenMinterContract :
require(gencore.hasArtistSigned(_collectionID), "artist didn't sign"); // custom error ideally
This would ensure the whole minting part of the process of emitting a collection will begin only after artist signed it.
INextGenCore interface should also be modified to add the function declaration:
function hasArtistSigned(uint256 _collectionID) external view returns (bool);
requestRandomWords()
functionRandomizerRNG contract, contrary to other randomizers, needs to send ETH when calling arrngController.requestRandomWords()
function, in order to request and then obtain random numbers to generate the unique hash of the token.
Currently, there are 2 payable functions : requestRandomWords()
and receive()
.
requestRandomWords()
is problematic as the only way to call it is through gencore by calling calculateTokenHash()
function, which is not payable. Therefore, it is only possible to send ether to this contract is to ''raw'' send ether, calling receive()
function.
In this configuration, there is no transparency in how the contract will be funded, by who, and users cannot be sure that the randomizer will always work, as it depends on its funding which is not programmatically determined.
minterContract
variable type in NextGenCore contract and gencore
variable type in AuctionDemo contract should be changed to be consistentNextGenCore declares in its storage 2 variables related to other contracts :
INextGenAdmins private adminsContract; address public minterContract;
Both are named as "contract", while their type is not consistent.
I recommend to replace address public minterContract
with IMinterContract public minterContract)
, and modify subsequent code where this variable is used (either to wrap it or to unwrap it).
In AuctionDemo contract, state variables are declared as follows :
IMinterContract public minter; INextGenAdmins public adminsContract; address gencore;
address gencore
should be replaced by INextGenCore public gencoreContract
or IERC721 public gencore
(after importing INextGenCore if the first solution is chosen). This would increase consistency. This variable is used only twice, and is wrapped each time into an IERC721
type. Changing the type of the variable would prevent from doing this unnecessary wrapping.
setCollectionCosts()
function is not consistent with collectionPhasesDataStructure
struct. The same applies for the order of return variables in retrieveCollectionPhases()
function.collectionPhasesDataStructure
struct is represented as follows :
struct collectionPhasesDataStructure { uint256 allowlistStartTime; uint256 allowlistEndTime; uint256 publicStartTime; uint256 publicEndTime; bytes32 merkleRoot; uint256 collectionMintCost; uint256 collectionEndMintCost; uint256 timePeriod; uint256 rate; uint8 salesOption; address delAddress; }
setCollectionCosts
function actually switched timePeriod
and rate
position :
function setCollectionCosts( uint256 _collectionID, uint256 _collectionMintCost, uint256 _collectionEndMintCost, uint256 _rate, uint256 _timePeriod, uint8 _salesOption, address _delAddress )
retrieveCollectionPhases()
function returns variables of the struct in the following order:
collectionPhases[_collectionID].allowlistStartTime, collectionPhases[_collectionID].allowlistEndTime, collectionPhases[_collectionID].merkleRoot, collectionPhases[_collectionID].publicStartTime, collectionPhases[_collectionID].publicEndTime
merkleroot
variable should be returned after publicEndTime
to increase consistency.
This is error-prone, and the order of parameters should be modified in setCollectionCosts()
function, as well as the order of return variables in retrieveCollectionPhases()
function.
#0 - 141345
2023-11-25T08:04:18Z
132 Tadev l r nc 6 0 3
L 1 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/998 L 2 l L 3 l L 4 n L 5 l L 6 l L 7 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/1138 L 8 l N 1 l N 2 n N 3 n
#1 - c4-pre-sort
2023-11-25T08:05:39Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T15:39:04Z
The Warden's QA report has been graded A based on a score of 24 combined with a manual review per the relevant QA guideline document located here.
The Warden's submission's score was assessed based on the following accepted findings:
requestRandomWords
Visibility#3 - c4-judge
2023-12-08T15:39:10Z
alex-ppg marked the issue as grade-a
13.9832 USDC - $13.98
address gencore
or INextGenCore public gencoreContract
should be removed from RandomizerNXT, RandomizerRNG and RandomizerVRF contracts.Declaring both the address and the contract instance will use 2 storage slots instead of one. The constructor will assign 2 values with no reason :
gencore = _gencore; gencoreContract = INextGenCore(_gencore);
Moreover, the updateCoreContract(address)
function updates both values, while only one would be enough.
If you only keep the address gencore
variable, just wrap it into a contract type when you need to call a function on it. While RandomizerNXT contract doesn't even use gencoreContract
and creates this variable with no reason, RandomizerRNG and RandomizerVRF contracts only use it once in fulfillRandomWords()
function. You could just rewrite the call to setTokenHash()
function in Randomizer contracts as follows :
INextGenCore(gencore).setTokenHash(...)
tokenToRequest
mapping in RandomizerRNG and RandomizerVRF contracts should be removedBoth RandomizerRNG and RandomizerVRF contracts declare a mapping mapping(uint256 => uint256) public tokenToRequest
, and it is only used to store requestId
in the requestRandomWords()
function. But this value is never used, and I don't see any utility in storing the requestId corresponding to a minted token in the Randomizer contract.
While the bot report suggests to merge these following 2 mappings using struct/nested mappings in both contracts (N33 and G04 issues):
mapping(uint256 => uint256) public tokenToRequest; mapping(uint256 => uint256) public tokenIdToCollection;
I propose to simply remove tokenToRequest
variable, as it is unused and has no utility. This will save gas compared to creating a merged data structure without the need to do it.
mint()
, burnOrSwapExternalToMint()
and payArtist()
functions in NextGenMinterContract and should be removed.At the beginning of the mint()
function in NextGenMinterContract contract, a variable called col
is declared and set :
uint256 col = _collectionID;
While _collectionID
variable could be used in the rest of the function, declaring col
costs gas without any reason. This variable should be removed, and _collectionID
should be used wherever col
is currently used.
The same applies for tokdata
variable, which is declared and assigned to _tokendata
:
This string variable tokdata
should also be removed, and _tokendata
should be used wherever tokdata
is currently used.
All this also applies for burnOrSwapExternalToMint()
function, with the same 2 variables.
For payArtist()
function, 3 variables are created inside the function, but this is redondant and they should me removed :
address tm1 = _team1; address tm2 = _team2; uint256 colId = _collectionID;
burnToMint()
and mintAndAuction()
functions in NextGenMinterContract. One of them should be removed.Both collectionTokenMintIndex
and mintIndex
variables are assigned to gencore.viewTokensIndexMin(_mintCollectionID) + gencore.viewCirSupply(_mintCollectionID)
.
This is redondant. Hence, mintIndex
should be removed, as there is an important check just after collectionTokenMintIndex
declaration and assignment. Also, mintIndex
should be replace where it used :
gencore.burnToMint(mintIndex, _burnCollectionID, _tokenId, _mintCollectionID, _saltfun_o, burner);
should be:
gencore.burnToMint(collectionTokenMintIndex, _burnCollectionID, _tokenId, _mintCollectionID, _saltfun_o, burner);
This is the case in both burnToMint()
and mintAndAuction()
functions.
mint()
function and burnToMint()
function in NextGenCore contract should be removedmint()
function and burnToMint()
function of NextGenCore contract both use an if statement to decide whether or not it should proceed to _mintprocessing()
and achieve minting process :
if ( collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply ) {...}
This check is actually not necessary, as:
mint()
function can only be called by mint()
or burnOrSwapExternalToMint()
functions of the Minter contractburnToMint()
function can only be called by burnToMint()
function of the Minter contract.All theses functions already make sure the totalSupply is not reached. Hence, there is no situation in which mint()
function or burnToMint()
function in NextGenCore contract can be called without satisfying the condition of this if statement.
Moreover, if calling theses functions was possible while totalSupply is reached, this would mean that it could be possible to increase collectionCirculationSupply
value beyond collectionTotalSupply
, as the _mintprocessing
function wouldn't be executed while collectionCirculationSupply
would be incremented by one, without any revert.
I suggest to remove this if statement that will consume gas for no reason.
collectionArtistPrimaryAddresses[_collectionID].status
variable to false
in proposePrimaryAddressesAndPercentages()
and proposeSecondaryAddressesAndPercentages()
functions in NextGenMinterContractBoth functions use this check :
require (collectionArtistPrimaryAddresses[_collectionID].status == false, "Already approved");
Nevertheless, status
is already false during the execution of this function as there is a check for that at the beginning of the function:
require (collectionArtistPrimaryAddresses[_collectionID].status == false, "Already approved");
Hence, the following line can be removed in both functions :
collectionArtistPrimaryAddresses[_collectionID].status = false;
#0 - c4-pre-sort
2023-11-27T14:52:32Z
141345 marked the issue as insufficient quality report
#1 - c4-judge
2023-12-02T18:10:23Z
alex-ppg marked the issue as grade-b