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: 34/243
Findings: 2
Award: $371.83
🌟 Selected for report: 0
🚀 Solo Findings: 0
109.1409 USDC - $109.14
Possible Optimization 1 =
bytes32
arrays for collectionScript. Strings are expensive in terms of gas usage, especially when dealing with arrays. Using bytes32
arrays can significantly reduce gas costs.Here is the optimized code snippet:
struct collectionInfoStructure { // ... other fields ... bytes32[] collectionScript; } function createCollection( string memory _collectionName, string memory _collectionArtist, string memory _collectionDescription, string memory _collectionWebsite, string memory _collectionLicense, string memory _collectionBaseURI, string memory _collectionLibrary, bytes32[] memory _collectionScript ) public FunctionAdminRequired(this.createCollection.selector) { // ... rest of the function ... }
bytes32
can save significant gas, especially for longer strings. The opcode SSTORE
costs are reduced due to fewer storage slots being used.Possible Optimization 2 =
mint
or airdrop
, consider updating it in batches. This reduces the number of state changes and hence the gas cost.Here is the optimized code:
function batchMint( uint256[] memory mintIndices, address[] memory recipients, string[] memory tokenDatas, uint256 collectionID, uint256 saltfun_o ) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); uint256 supplyIncrement = mintIndices.length; collectionAdditionalData[collectionID].collectionCirculationSupply += supplyIncrement; for (uint256 i = 0; i < supplyIncrement; i++) { _mintProcessing(mintIndices[i], recipients[i], tokenDatas[i], collectionID, saltfun_o); } }
SSTORE
operations. Each SSTORE
operation costs 20,000 gas for a non-zero value being set. By batching, you save on these costs per mint/airdrop.Possible Optimization 3 =
Optimized Code Snippet:
function updateCollectionInfo( uint256 _collectionID, string memory _newCollectionName, // ... other parameters ... ) public CollectionAdminRequired(_collectionID, this.updateCollectionInfo.selector) { require( collectionFreeze[_collectionID] == false && isCollectionCreated[_collectionID] == true, "Not allowed" ); // ... rest of the function ... }
Possible Optimization 4 =
SSTORE
operations, which are costly in terms of gas. Use a temporary struct in memory to accumulate changes and then assign it to the storage in one operation. This reduces the number of SSTORE
operations.After Optimization:
function createCollection(string memory _collectionName, string memory _collectionArtist, string memory _collectionDescription, string memory _collectionWebsite, string memory _collectionLicense, string memory _collectionBaseURI, string memory _collectionLibrary, string[] memory _collectionScript) public FunctionAdminRequired(this.createCollection.selector) { collectionInfoStructure memory newCollection = collectionInfoStructure({ collectionName: _collectionName, collectionArtist: _collectionArtist, collectionDescription: _collectionDescription, collectionWebsite: _collectionWebsite, collectionLicense: _collectionLicense, collectionBaseURI: _collectionBaseURI, collectionLibrary: _collectionLibrary, collectionScript: _collectionScript }); collectionInfo[newCollectionIndex] = newCollection; isCollectionCreated[newCollectionIndex] = true; newCollectionIndex = newCollectionIndex + 1; }
Possible Optimization 5 =
SSTOREs
in different scenarios. Similar to the previous optimization, use a temporary struct in memory to accumulate changes and then assign it to the storage in one operation.Optimized Code Snippet:
function setCollectionData(uint256 _collectionID, address _collectionArtistAddress, uint256 _maxCollectionPurchases, uint256 _collectionTotalSupply, uint _setFinalSupplyTimeAfterMint) public CollectionAdminRequired(_collectionID, this.setCollectionData.selector) { require((isCollectionCreated[_collectionID] == true) && (collectionFreeze[_collectionID] == false) && (_collectionTotalSupply <= 10000000000), "err/freezed"); collectionAdditonalDataStructure memory newData = collectionAdditionalData[_collectionID]; newData.collectionArtistAddress = _collectionArtistAddress; newData.maxCollectionPurchases = _maxCollectionPurchases; newData.setFinalSupplyTimeAfterMint = _setFinalSupplyTimeAfterMint; if (newData.collectionTotalSupply == 0) { newData.collectionCirculationSupply = 0; newData.collectionTotalSupply = _collectionTotalSupply; newData.reservedMinTokensIndex = (_collectionID * 10000000000); newData.reservedMaxTokensIndex = (_collectionID * 10000000000) + _collectionTotalSupply - 1; } collectionAdditionalData[_collectionID] = newData; wereDataAdded[_collectionID] = true; }
Possible Optimization 1 =
payout
. We can optimize it by calculating percentages in a more gas-efficient manner.After Optimization:
function payArtist(uint256 _collectionID, address _team1, address _team2, uint256 _teamperc1, uint256 _teamperc2) public FunctionAdminRequired(this.payArtist.selector) { // ... existing checks ... uint256 totalAmount = collectionTotalAmount[_collectionID]; uint256 onePercent = totalAmount / 100; artistRoyalties1 = onePercent * collectionArtistPrimaryAddresses[colId].add1Percentage; artistRoyalties2 = onePercent * collectionArtistPrimaryAddresses[colId].add2Percentage; artistRoyalties3 = onePercent * collectionArtistPrimaryAddresses[colId].add3Percentage; teamRoyalties1 = onePercent * _teamperc1; teamRoyalties2 = onePercent * _teamperc2; // ... existing logic to transfer funds ... }
Possible Optimization 2 =
for-loop
that calls the gencore.mint
function for each token to be minted. This can be optimized by batch processing within the gencore
contract, if possible, to reduce the number of external contract calls.After:
// Assuming gencore contract has a batchMint function implemented // If not I suggest you add it for efficiency as detailed in optimizations for NextGenCore function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { // ... existing checks and logic ... uint256[] memory mintIndices = new uint256[](_numberOfTokens); for(uint256 i = 0; i < _numberOfTokens; i++) { mintIndices[i] = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col) + i; } gencore.batchMint(mintIndices, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase); // ... rest of the function ... }
Possible Optimization 1 =
_msgSender() == owner()
is more expensive than adminPermissions[msg.sender]
.After Optimization:
modifier AdminRequired { require(adminPermissions[msg.sender] || _msgSender() == owner(), "Not allowed"); _; }
msg.sender
is an admin because it avoids the need to check the owner status, which includes an SLOAD
operation. The exact savings depend on the frequency of this check but are roughly around 800-2000 gas.Possible Optimization 2 =
functionAdmin[_address]
mapping to avoid multiple mapping lookups.After:
function registerBatchFunctionAdmin(address _address, bytes4[] memory _selector, bool _status) public AdminRequired { mapping(bytes4 => bool) storage functionPermissions = functionAdmin[_address]; for (uint256 i = 0; i < _selector.length; i++) { functionPermissions[_selector[i]] = _status; } }
Possible Optimization =
After Optimization:
function calculateTokenHash(uint256 _collectionID, uint256 _mintIndex, uint256 _saltfun_o) public { require(msg.sender == gencore); // Assuming randoms.randomNumber() and randoms.randomWord() return uint256. uint256 randomInput = randoms.randomNumber() ^ randoms.randomWord(); bytes32 hash = keccak256(abi.encodePacked(_mintIndex, blockhash(block.number - 1), randomInput)); gencoreContract.setTokenHash(_collectionID, _mintIndex, hash); }
Possible Optimization =
After Optimization:
function updateContracts(address _newAdminsContract, address _newGencore) public FunctionAdminRequired(this.updateContracts.selector) { if(_newAdminsContract != address(adminsContract)) { require(INextGenAdmins(_newAdminsContract).isAdminContract() == true, "Contract is not Admin"); adminsContract = INextGenAdmins(_newAdminsContract); } if(_newGencore != gencore) { gencore = _newGencore; gencoreContract = INextGenCore(_newGencore); } }
Possible Optimization =
Here is the optimized code snippet:
function updateContracts(address _newAdminsContract, address _newGencore) public FunctionAdminRequired(this.updateContracts.selector) { bool updateAdmin = (_newAdminsContract != address(adminsContract)) && INextGenAdmins(_newAdminsContract).isAdminContract(); bool updateGencore = (_newGencore != gencore); if(updateAdmin) { adminsContract = INextGenAdmins(_newAdminsContract); } if(updateGencore) { gencore = _newGencore; gencoreContract = INextGenCore(_newGencore); } }
Possible Optimization 1 =
After Optimization:
string[100] private constant wordsList = [ "Acai", "Ackee", "Apple", // ... other fruits ... "Watermelon" ]; function getWord(uint256 id) private view returns (string memory) { require(id < wordsList.length, "Index out of bounds"); return wordsList[id]; }
Possible Optimization 2 =
After:
// Example using Chainlink VRF; this requires additional setup such as funding the contract with LINK // and setting up a subscription on the Chainlink VRF service. function requestRandomWord() public returns (bytes32 requestId) { // Chainlink VRF call requestId = COORDINATOR.requestRandomWords( keyHash, subscriptionId, requestConfirmations, callbackGasLimit, numWords ); // Additional logic to handle the requestId and randomness fulfillment }
Possible Optimization =
After Optimization:
// The claimAuction function can be simplified as follows: function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ require(block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true); auctionClaim[_tokenid] = true; uint256 highestBid = highestBids[_tokenid].bid; address highestBidder = highestBids[_tokenid].bidder; // Transfer token and funds only for the highest bid IERC721(gencore).safeTransferFrom(IERC721(gencore).ownerOf(_tokenid), highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}(""); require(success, "Payment to owner failed"); emit ClaimAuction(owner(), _tokenid, success, highestBid); }
#0 - c4-pre-sort
2023-11-21T12:44:26Z
141345 marked the issue as high quality report
#1 - 141345
2023-11-26T06:04:35Z
1766 K42 l r nc 3 9 3
G 1 n G 2 r G 3 n G 4 l G 5 l G 1 r G 2 r G 1 l G 2 r G 1 n G 1 r G 1 r G 1 r G 2 r G 1 r
#2 - c4-pre-sort
2023-11-26T06:04:47Z
141345 marked the issue as sufficient quality report
#3 - c4-pre-sort
2023-11-26T06:04:53Z
141345 marked the issue as high quality report
#4 - c4-sponsor
2023-11-28T06:46:58Z
a2rocket (sponsor) acknowledged
#5 - alex-ppg
2023-12-02T18:08:27Z
NextGenCore.sol
storage
pointer to avoid memory expansionRandomizerNXT.sol
RandomizerVRF.sol
RandomizerRNG.sol
XRandoms.sol
string
array types cannot be set as constant#6 - c4-judge
2023-12-02T18:08:32Z
alex-ppg marked the issue as grade-a
🌟 Selected for report: MrPotatoMagic
Also found by: 3th, Fulum, JohnnyTime, K42, Kose, SAAJ, Toshii, catellatech, cats, clara, digitizeworx, dimulski, dy, glcanvas, hunter_w3b, ihtishamsudo, niki, peanuts
262.6934 USDC - $262.69
The system comprises:
NextGenCore Contract:
MinterContract:
NextGenAdmins:
Randomizer Contracts (RandomizerNXT, RandomizerVRF, RandomizerRNG):
XRandoms:
AuctionDemo:
NextGen contracts provide a solid foundation for an NFT ecosystem but require further security measures, optimizations, and a reevaluation of admin centralization and upgradeability.
20 hours
#0 - c4-pre-sort
2023-11-27T14:36:52Z
141345 marked the issue as insufficient quality report
#1 - c4-judge
2023-12-07T16:49:11Z
alex-ppg marked the issue as grade-a