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: 5/243
Findings: 7
Award: $1,930.27
🌟 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/ff8cfc5529ee4a567e1ce1533b4651d6626d1def/smart-contracts/NextGenCore.sol#L182-L183 https://github.com/code-423n4/2023-10-nextgen/blob/ff8cfc5529ee4a567e1ce1533b4651d6626d1def/smart-contracts/NextGenCore.sol#L193-L198 https://github.com/code-423n4/2023-10-nextgen/blob/ff8cfc5529ee4a567e1ce1533b4651d6626d1def/smart-contracts/NextGenCore.sol#L218-L221
The MinterContract has a public mint function which can be called by anyone to mint themself a NextGenCore NFT token against a certain amount of ETH. The mint function in the ERC721 contract does not follow the CEI pattern. It first makes a call to ERC721's _safeMint thorugh the _mintProcessing function.
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; } } }
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); }
This lets a malicious party reenter the execution with the onERC721Received hook. This can cause a variety of problems. Let's look at one of them:
During a public minting phase, each Ethereum account must not be able to mint more NFTs than
collectionAdditionalData[_collectionID].maxCollectionPurchases
This invariant is enforced by the MinterContract by comparing the amount that the user will have if the mint is successful with the maxCollectionPurchases:
require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");
As we have already seen, the minted tokens per address are updated after the safeMint which means that this require statement can be bypassed from a contract that mints as long as it has funds to do so.
forge test --lib-paths ../smart-contracts
Foundry
Reentrancy
#0 - c4-pre-sort
2023-11-15T12:53:15Z
141345 marked the issue as primary issue
#1 - c4-pre-sort
2023-11-15T12:54:11Z
141345 marked the issue as sufficient quality report
#2 - c4-sponsor
2023-11-22T12:26:30Z
a2rocket (sponsor) confirmed
#3 - c4-pre-sort
2023-11-26T14:04:34Z
141345 marked the issue as duplicate of #1742
#4 - c4-judge
2023-12-08T16:39:18Z
alex-ppg marked the issue as satisfactory
#5 - c4-judge
2023-12-08T16:40:32Z
alex-ppg marked the issue as partial-50
#6 - c4-judge
2023-12-08T19:17:25Z
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#L125 https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L135
The AuctionDemo contract has functions for claiming an auction canceling bids. An auction has an end time. Claiming an auction can happen only when the auction has ended and canceling bid can happen only when the auction is active. However, both these functionalities use <= to check the current timestamp value against the end time of the auction.
AuctionDemo.participateToAuction():
require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true);
AuctionDemo.cancelBid():
require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");
A certain edge case, when the block.timestamp value is equal to the end time allows claiming auction and canceling all bids after that. This will:
forge test --lib-paths ../smart-contracts
Foundry
Decide if the end time of the auction is inclusive or exclusive and remove the = from the needed function.
Timing
#0 - c4-pre-sort
2023-11-14T23:40:30Z
141345 marked the issue as duplicate of #962
#1 - c4-judge
2023-12-01T16:08:47Z
alex-ppg marked the issue as not a duplicate
#2 - c4-judge
2023-12-01T16:08:55Z
alex-ppg marked the issue as duplicate of #1788
#3 - c4-judge
2023-12-08T18:23:20Z
alex-ppg marked the issue as partial-50
#4 - c4-judge
2023-12-09T00:20:29Z
alex-ppg changed the severity to 3 (High Risk)
🌟 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
When an auction comes to its end, the highest bid is sent to the owner of the NFT selled and all other bids are send back to their owners. A gas griefing attack where a malicious bidder consumes all the gas available is possible. All auctions can be DOS-ed if a malicious bidder sends as little as 1 wei to the contract. Once entered, bidders cannot be removed. The bid call can be initiated by a contract that uses up all the available gas on receive(). The attacker can even bid several times to increase even further their chances of DOS of the system (because of the 63/64 rule) or just gas bomb it returning a huge amount of bytes.
Impact -> all the gas will be consumed and the transaction will revert, the NFT will not be send to the winner and the remainder of the funds will be stuck forever.
forge test --lib-paths ../smart-contracts
Foundry
Add a mapping that tracks the balance of each bidder. When an auction ends, increase the corresponding balances. Then create a function that enables withdrawals from the contract by the authorized bidders.
DoS
#0 - c4-pre-sort
2023-11-15T12:27:34Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:52Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-01T22:50:56Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-01T22:51:21Z
alex-ppg marked the issue as duplicate of #1782
#4 - c4-judge
2023-12-08T20:54:30Z
alex-ppg marked the issue as partial-50
#5 - ZdravkoHr
2023-12-09T12:36:21Z
This issue should not be labeled as partial-50. It shows the same root cause and impact as 734.
Thanks!
#6 - alex-ppg
2023-12-09T16:09:14Z
Hey @ZdravkoHr, thanks for your contribution! The submission was graded partially because of an "incorrect" recommendation chapter.
There is no need to maintain mappings that are solely mutated when the auction ends, better alternatives exist as advised by other duplicated submissions. Additionally, the impact specifies that the "remainder" will be left in the contract which is not the case as all funds will be locked, not just some.
1114.2534 USDC - $1,114.25
According to the gitbook, minting of NFTs can happen at different phases. Each phase has configurations for its minting periods and the sale mode. Before a minting phase starts, these configurations have to be set by calling MinterContract.setCollectionCosts() and MinterContract.setCollectionPhases(). This is where the sales mode is chosen.
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"); 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; }
The periodic scale mode allows minting 1 NFT per time period. The problem lies in the way the calculation for when the last mint has happened is done. It uses all the tokens in circulation, including ones that have been minted in different phases. If a minting with different sale mode has happened before the periodic scale, this calculation will become broken and users will not be able to mint.
lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));
forge test --lib-paths ../smart-contracts
Foundry
Add internal counter of all the NFTs that have been minted during the periodic scale phase. Use it instead of the total circulation. Reset this counter when changes are made to the costs or phases.
Context
#0 - c4-pre-sort
2023-11-15T12:52:27Z
141345 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T14:14:27Z
141345 marked the issue as primary issue
#2 - c4-sponsor
2023-11-22T12:27:16Z
a2rocket (sponsor) confirmed
#3 - c4-judge
2023-12-05T13:18:41Z
alex-ppg marked the issue as duplicate of #2012
#4 - c4-judge
2023-12-08T21:08:43Z
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
When an auction comes to an end, the AuctionDemo.claimAuction() can be called to transfer the NFT to the winner and returns the bids of the other bidders. If the winner of the auction is a contract that either reverts on received ERC721 token or does not implement the onERC721Received function (may be a simple mistake, no malicious intent behind it), the funds of the other bidders will be stuck forever in the contract.
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 = returnHighestBid(_tokenid); address ownerOfToken = IERC721(gencore).ownerOf(_tokenid); address highestBidder = returnHighestBidder(_tokenid); 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) { (bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}(""); emit Refund(auctionInfoData[_tokenid][i].bidder, _tokenid, success, highestBid); } else {} } }
Manual Review
Add another admin protected function which distributes all the funds to the bidders that have not won the auction after it has ended. I suggest adding another mapping which tracks if a given auction has failed to transfer an NFT. The default will be false. The transfer of the NFT will be wrapped in a try/catch where the catch sets the flag to true. Then our function will check if the flag is true and only then execute its logic. This will not add any new centralization risks.
ERC721
#0 - c4-pre-sort
2023-11-15T10:39:22Z
141345 marked the issue as duplicate of #843
#1 - c4-pre-sort
2023-11-16T13:35:16Z
141345 marked the issue as duplicate of #486
#2 - c4-judge
2023-12-05T22:19:07Z
alex-ppg marked the issue as not a duplicate
#3 - c4-judge
2023-12-05T22:19:16Z
alex-ppg marked the issue as duplicate of #739
#4 - c4-judge
2023-12-08T22:21:59Z
alex-ppg marked the issue as partial-50
#5 - ZdravkoHr
2023-12-09T12:06:27Z
This issue shows the same impacts as 739:
It only does not mention that a protocol invariant is broken. I think it does not deserve the partial-50 label. According to the docs, a partial label can be added when the top identified severity case is not present:
However, any submissions which do not identify or effectively rationalize the top identified severity case may be judged as “partial credit” and may have their shares in that finding’s pie divided by 2 or 4 at judge’s sole discretion (e.g. 50% or 25% of the shares of a satisfactory submission in the duplicate set).
Thanks!
#6 - alex-ppg
2023-12-09T16:04:51Z
Hey @ZdravkoHr, thanks for your feedback! The relevant documentation cited uses may
deliberately as it is not a hard rule. I will cite the relevant Supreme Court ruling and maintain that this submission is not of equivalent quality to the top issue.
Namely, the submission fails to identify a valid reason to do this (i.e. blackmail) and is of overall lower quality than its peers. Given the myriad of duplicates of this submission, I opted to award high-quality submissions with a full reward whilst awarding lesser-quality submissions with partials.
800.6263 USDC - $800.63
During a linear descending [sales mode], the mint price decreases on each mint period until it reaches a given endPrice. This is achieved by the MinterContract.getPrice() function. It divides the endMintCost by the rate, which results in over how many periods will the mint cost reach the endMintCost.
if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) / (collectionPhases[_collectionId].rate)) > tDiff) { price = collectionPhases[_collectionId].collectionMintCost - (tDiff * collectionPhases[_collectionId].rate); } else { price = collectionPhases[_collectionId].collectionEndMintCost; }
The above if uses ** > ** instead of ** >= ** to check if the totalPeriods are greater than the passed periods. This will skip the last period before the final cost. If the selling is set to have 1 decreasing phase, the endCost / rate will be 1. tDiff will also be 1. 1 > 1 => false. In result, minters will be able to mint paying end price earlier.
forge test --lib-paths ../smart-contracts
Foundry
Use ** >= ** instead of ** > **:
- if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) - / (collectionPhases[_collectionId].rate)) > tDiff) { + if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) + / (collectionPhases[_collectionId].rate)) >= tDiff) {
Math
#0 - c4-pre-sort
2023-11-15T12:56:13Z
141345 marked the issue as duplicate of #1391
#1 - c4-pre-sort
2023-11-26T12:32:24Z
141345 marked the issue as not a duplicate
#2 - c4-pre-sort
2023-11-26T12:32:42Z
141345 marked the issue as duplicate of #393
#3 - c4-judge
2023-12-08T22:35:36Z
alex-ppg marked the issue as satisfactory
🌟 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
The MinterContract allows airdrops. The airdrop function loops through every recipient and mints him an amount of NFTs. If one of the receivers does not implement the ERC721 onERC721Received or implements it to always revert, the whole airdrop will fail. Consider using Pull over Push
Since an NFTs hash in XRandoms.sol is generated by a mintIndex, block.prevrandao, blockhash and block.number, all NFTs generated in the same block will have the same preimage except for mintIndex. Because mintIndex cannot be the same for 2 tokens, this is a finding of LOW severity.
The XRandoms.getWord function stores 100 words as strings in an array. It takes an id parameter and returns words[id] if the id is 0 and words[id - 1] otherwise. The function is private, so it can be called only from the same contract. The only place where it is called is the XRandoms.randomWord with a value from 0-99. The last word is at index 99. It can never be accessed because the id is subtracted by 1.
There are functions that are protected by a modifier that checks their signatures. The caller is able to execute the function only if he is whitelisted for its signature. With adding more and more functions, the possibility of function selector clash increases. Consider mixing the signature with an additional element.
Currently, all files are stored in the root folders. Consider organizing them into different folders. For example, libs, randoms, core
The code is not formatted properly. Consider using a tool that formats it.
#0 - 141345
2023-11-25T08:30:48Z
178 ZdravkoHr l r nc 0 0 0
L 1 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/486 L 2 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/1818 L 3 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/508 L 4 d dup of https://github.com/code-423n4/2023-10-nextgen-findings/issues/1115 L 5 i L 6 i
#1 - c4-pre-sort
2023-11-25T08:46:34Z
141345 marked the issue as sufficient quality report
#2 - alex-ppg
2023-12-08T15:37:41Z
The Warden's QA report has been graded B based on a score of 16 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:37:51Z
alex-ppg marked the issue as grade-b