NextGen - 0x3b's results

Advanced smart contracts for launching generative art projects on Ethereum.

General Information

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

NextGen

Findings Distribution

Researcher Performance

Rank: 1/243

Findings: 8

Award: $6,215.77

QA:
grade-b

🌟 Selected for report: 3

🚀 Solo Findings: 0

Lines of code

(https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/NextGenCore.sol#L193-L196

Vulnerability details

Impact

Whitelisted users can mint before the public mint starts, but they are restricted in the number of NFTs they can mint. However, they will be able to re-enter the mint function and mint as many NFTs as they want.

Proof of Concept

When the mint function is called, users need to verify their order with the Merkle tree by inputting their correct _maxAllowance and tokData. After they are verified, gencore.mint is called to mint their tokens. However, before increasing tokensMintedAllowlistAddress, _mintProcessing is called.

            _mintProcessing(mintIndex, _mintTo, _tokenData, _collectionID, _saltfun_o);
            if (phase == 1) {
                tokensMintedAllowlistAddress[_collectionID][_mintingAddress] += 1;
            }

This, in turn, mints the user their NFT.

    function _mintProcessing(...) internal {
        tokenData[_mintIndex] = _tokenData;
        collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o);
        tokenIdsToCollectionIds[_mintIndex] = _collectionID;
        _safeMint(_recipient, _mintIndex);
    }

Because it uses safeMint, users are called by the 721 Contract with _checkOnERC721Received. This call can be caught in the attacker's contract, and from there, reentrancy begins. After the mint is done, the attacker can call MinterContract.mint again, and the requirement that checks their max mint limit will pass.

    } else {
         node = keccak256(abi.encodePacked(msg.sender, _maxAllowance, tokData));
         //@audit if we re-enter `retrieveTokensMintedALPerAddress` will return the not yet updated value
         require(_maxAllowance >= gencore.retrieveTokensMintedALPerAddress(col, msg.sender) + _numberOfTokens, "AL limit");
         mintingAddress = msg.sender;
    }

This is because we re-entered before tokensMintedAllowlistAddress had the chance to increase, which means that gencore.retrieveTokensMintedALPerAddress still has the old value.

Due to this reentrancy, whitelisted users can mint more NFTs than they should. This will also have an effect on phase 3 sales, enabling users to mint NFTs before the price has had a chance to update. Overall, this breaks the concept of users being restricted in the number of NFTs they can mint.

Tools Used

Manual review.

Add nonReentrant in the minting functions.

Assessed type

Reentrancy

#0 - c4-pre-sort

2023-11-20T14:35:15Z

141345 marked the issue as duplicate of #51

#1 - c4-pre-sort

2023-11-26T13:59:53Z

141345 marked the issue as duplicate of #1742

#2 - c4-judge

2023-12-08T16:38:14Z

alex-ppg marked the issue as satisfactory

#3 - c4-judge

2023-12-08T16:40:29Z

alex-ppg marked the issue as partial-50

#4 - c4-judge

2023-12-09T00:18:52Z

alex-ppg changed the severity to 3 (High Risk)

Findings Information

🌟 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

Awards

0 USDC - $0.00

Labels

bug
3 (High Risk)
satisfactory
edited-by-warden
duplicate-1323

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L125

Vulnerability details

Impact

Due to claimAuction and cancelBid both using = in their time equations, bidders will be able to double refunds their bids.

function claimAuction(...) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
require(block.timestamp >= minter.getAuctionEndTime(_tokenid)...);

function cancelBid(...) public {...
require(block.timestamp <= minter.getAuctionEndTime(_tokenid)...);

Proof of Concept

The winner is able to claim the auction at it's exact auction end time.

function claimAuction(...) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
require(block.timestamp >= minter.getAuctionEndTime(_tokenid)...);

However bidders are also able to cancel their bids at exact auction end time.

function cancelBid(...) public {...
require(block.timestamp <= minter.getAuctionEndTime(_tokenid)...);

This means that the winner can claim, and him or some bidder to double refunds their bids. This is done by first using the refund in claimAuction and then canceling his bid with cancelBid.

A few example scenarios occur, where in order to do this you must have some failed (not winning) bids:

  1. The winner can make multiple bids and either re-enter here after every not winning bid and refund it, or simply schedule in one TX claimAuction and cancelBid on all his not winning bids.

  2. Users can re-enter their failed bids and refunds them.

  3. Seller of the NFT can re-enter and claim his failed bids.

  4. Users can back-run the claimAuction call, and as long as they are with the same timestamp they will be able to cancelBid.

Tools Used

Manual review

Even if the re-entracy is fixed the winner would still be able to schedule claimAuction and cancelBid in one TX and double refund his other bids. The main issue is <= which needs to be made < in cancelBid.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-14T23:44:07Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-04T21:39:58Z

alex-ppg marked the issue as duplicate of #1323

#2 - c4-judge

2023-12-08T18:24:06Z

alex-ppg marked the issue as satisfactory

Findings Information

🌟 Selected for report: 0x3b

Also found by: 0xlemon, AvantGard, Krace, MrPotatoMagic, Noro, ZdravkoHr, fibonacci, nuthan2x, oakcobalt, trachev

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
sufficient quality report
upgraded by judge
edited-by-warden
H-04

Awards

1448.5295 USDC - $1,448.53

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L252

Vulnerability details

Impact

As explained by the sponsor, some collections might want to conduct multiple mints on different days. However, due to the way salesOption 3 works, these multiple mints might encounter issues.

Proof of Concept

A collection has completed its first mint, where it minted 500 NFTs. However, the collection consists of 1000 NFTs, so the owner plans to schedule another mint, this time using sales option 3.

Values
allowlistStartTime4 PM
allowlistEndTime7 PM
publicStartTime7 PM
publicEndTime1 day after public start
timePeriod1 min

The first user's mint will proceed smoothly since timeOfLastMint falls within the previous mint period. However, the second user's mint will fail. The same applies to all other whitelisted users. This issue arises due to the following block:

lastMintDate[col] = collectionPhases[col].allowlistStartTime
                + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));

This calculation extends the allowed time significantly, granting the second minter an allowed time of allowlistStartTime + 1 min * (500-1) = allowlistStartTime + 499 min, which is equivalent to 8 hours and 19 minutes after allowlistStartTime. This enables the second user to mint at 12:19 AM, long after the whitelist has ended and in the middle of the public sale. And if anyone tries to mint his call will revert with underflow error, as timeOfLastMint > block.timestamp.

uint256 tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[col].timePeriod;

It's worth noting that some collections may disrupt the whitelist, while others could brick the entire mint process, especially if there are more minted NFTs or a longer minting period.

POC

Gits - https://gist.github.com/0x3b33/677f86f30603dfa213541cf764bbc0e8 Add to remappings - contracts/=smart-contracts/ Run it with forge test --match-test test_multipleMints --lib-paths ../smart-contracts

Tools Used

Manual review.

For this fix I am unable to give any suggestion as big parts of the protocol need to be re-done. I can only point out the root cause of the problem, which is (gencore.viewCirSupply(col) - 1) in the snippet below.

lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));

Assessed type

Error

#0 - c4-pre-sort

2023-11-20T14:30:09Z

141345 marked the issue as sufficient quality report

#1 - c4-sponsor

2023-11-23T09:30:03Z

a2rocket (sponsor) confirmed

#2 - c4-judge

2023-12-05T23:11:19Z

alex-ppg marked the issue as duplicate of #2012

#3 - c4-judge

2023-12-07T12:18:46Z

alex-ppg marked the issue as selected for report

#4 - alex-ppg

2023-12-07T12:25:04Z

The Warden's submission was selected as the best given that it illustrates the problem by citing the relevant documentation of the project, contains a valid PoC, and acknowledges the difficulty in rectifying this issue. While the submission has under-estimated the issue's severity, the relevant high-severity issues (#2012, #1123, #939, #632, #631, #89) were not of sufficient quality and the best candidate (#1123) minimizes the issue's applicability and does not advise a proper recommendation either.

To alleviate the issue, the Sponsor is advised to implement a "start date" for the periodic sales that is reconfigured whenever a periodic sale is re-instated. This would permit the lastMintDate calculations to "restart" the date from which periodic sale allowances should be tracked and also allow the code to snapshot the circulating supply at the time the first periodic sale occurs of each independent periodic sale phase. As the Warden correctly assessed, a viable solution to this vulnerability is difficult to implement.

#5 - c4-judge

2023-12-07T12:25:12Z

alex-ppg marked the issue as satisfactory

#6 - c4-judge

2023-12-07T12:25:24Z

alex-ppg changed the severity to 3 (High Risk)

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L104-L120

Vulnerability details

Impact

Winners will be able to DoS auctions and lock both their NFTs and all of the funds of other bidders. This issue arises because AuctionDemo's claimAuction uses push instead of a pull.

The winner does not need to be a user, it can also be a competitive entity. A competitor against the collection or against NextGen.

Proof of Concept

Once the auction has ended, the winner can invoke claimAuction. And to revert on his _checkOnERC721Received.

The core issue is in this if if, within this for loop, where, if the current bidder is not the winner, their funds are refunded.

for (uint256 i=0; i< auctionInfoData[_tokenid].length; i++) {
    ...
    } 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 {}
}

Example:

  1. NFT is scheduled for auction with start of 0.01ETH
  2. 50 more bidders bid - 0.011, 0.015... 0.29.
  3. Attacker bids last - 0.3ETH (1 wei higher than the bid before him) using a contract that reverts when it receives the NFT.
  4. Auction ends.
  5. claimAuction reverts as the call made to the attacker contract reverts.

Attacker loses some ETH, however 50 bidders lose their bids (the last one is the same as the attaker) and the NFT will remain stuck in the contract.

Tools Used

Manual review

Utilize the pull method instead of the push method. This can be achieved by creating another function that allows users to manually withdraw their funds after the auction concludes.

Assessed type

DoS

#0 - c4-pre-sort

2023-11-15T10:45:29Z

141345 marked the issue as duplicate of #843

#1 - c4-pre-sort

2023-11-16T13:35:04Z

141345 marked the issue as duplicate of #486

#2 - c4-judge

2023-12-05T22:21:29Z

alex-ppg marked the issue as not a duplicate

#3 - c4-judge

2023-12-05T22:21:38Z

alex-ppg marked the issue as duplicate of #739

#4 - c4-judge

2023-12-08T22:22:57Z

alex-ppg marked the issue as partial-50

#5 - c4-judge

2023-12-09T00:23:13Z

alex-ppg changed the severity to 2 (Med Risk)

Findings Information

🌟 Selected for report: 0x3b

Also found by: AvantGard, lanrebayode77

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
edited-by-warden
M-08

Awards

3701.5187 USDC - $3,701.52

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L536

Vulnerability details

Impact

As explained by the docs, several steps can occur during the minting process. However, an airdrop before salesOption 3 can lead to price inflation.

Proof of Concept

Under salesOption 3, getPrice returns:

return collectionPhases[_collectionId].collectionMintCost
                    + ((collectionPhases[_collectionId].collectionMintCost / collectionPhases[_collectionId].rate) * gencore.viewCirSupply(_collectionId));

This is the increased rate based on the NFTs already in circulation. If an airdrop occurs before a mint with salesOption 3, the price will be much higher than intended.

Example:

StepsOptionNFTsPriceRate
1 OG usersAirdropped20 NFTsfree-
2 WhitelistedSales 310 NFTs1 ETH10 (0.1 ETH increase)
3 PublicSales 170 NFTs2 ETH-

With the current three steps, after the airdrop, salesOption 3 should start at 1 ETH and gradually increase to 2 ETH. Afterward, it should mint at a constant rate of 2 ETH.

However, when sales option 3 starts, getPrice will return 3 ETH instead of 1 ETH. This will cause the initial users to pay an inflated price, which was not intended by the owner and can harm their reputation. It's also unfair to the users, as these so-called special (whitelisted) users will pay increased prices.

collectionMintCost+(collectionMintCostrate)×cirSupply=1eth+0.1eth×20=1eth+2eth=3eth\begin{align*} \text{collectionMintCost} + \left(\frac{\text{collectionMintCost}}{\text{rate}}\right) \times \text{cirSupply} \\ &= 1 \text{eth} + 0.1 \text{eth} \times 20 \\ &= 1 \text{eth} + 2 \text{eth} \\ &= 3 \text{eth} \end{align*}

POC

Gist - https://gist.github.com/0x3b33/558b919a57101e7a0942e557a464078a Add to remappings - contracts/=smart-contracts/ Run it with forge test --match-test test_airdrop --lib-paths ../smart-contracts

Tools Used

Manual review.

You can implement a mapping with the airdropped NFTs and deduct this value from gencore.viewCirSupply(_collectionId) to avoid disrupting the minting process.

Assessed type

Error

#0 - c4-pre-sort

2023-11-16T10:25:09Z

141345 marked the issue as duplicate of #643

#1 - c4-judge

2023-12-06T13:01:57Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-06T13:02:46Z

alex-ppg marked the issue as duplicate of #881

#3 - c4-judge

2023-12-07T12:00:11Z

alex-ppg marked the issue as not a duplicate

#4 - c4-judge

2023-12-07T12:00:21Z

alex-ppg marked the issue as duplicate of #246

#5 - c4-judge

2023-12-07T12:14:33Z

alex-ppg marked the issue as selected for report

#6 - c4-judge

2023-12-07T12:25:13Z

alex-ppg marked the issue as satisfactory

#7 - alex-ppg

2023-12-07T12:25:34Z

After further consideration, I have decided to merge several periodic mint-related findings into two separate categories:

  • Incorrect Price Calculations (#381)
  • Incorrect Enforcement of Periodic Mint Limitations (#380)

The reasoning behind this change is that both rely on different aspects of the code and have different root causes. The former highlights a flaw in the getPrice function and how it calculates prices, whilst the latter highlights a flaw in the mint function and how it calculates the lastMintDate. Fixing one does not infer that the other is fixed, reinforcing the idea that they are separate vulnerabilities.

I consider this submission to be of a lower severity than #380 correctly given that it can be rectified. Specifically, a lastMintDate update in the future per #2012 cannot be reversed, while an incorrect price as indicated in this and relevant submissions can be reversed by reconfiguring the collection and can be controlled by the user simply not willing to pay the inflated price until the price is corrected.

A problem when selecting the best report in this submission is that all Warden submissions make mention of "airdropped" tokens and fail to identify that the general circulating supply affects the price in their proposed remediation, such as #380. I have thus chosen this report as the best given that it cites the relevant documentation, contains a privately accessible valid PoC, and provides a basic representation of the pricing formula using mathematical notation to illustrate the problem.

#8 - mcgrathcoutinho

2023-12-09T16:26:13Z

Hi @alex-ppg, here is why I believe this issue (and its duplicates) should be marked as a duplicate of #380 but with a partial grading.

  1. The root cause is the same i.e. existing circulating supply causes sale model 3 to work incorrectly.
  2. The impact mentioned here and that in #380 are two sides of the same coin.
  3. The issue mentions that prices will skyrocket (which is true) but the issue also mentions that initial users to pay an inflated price, which is incorrect since the user cannot mint (pay) in the first place due to the lastMintDate being set to a date far ahead in the future.
  4. I mention partial grading because although the root cause is correct, the impact demonstrated is invalid. Thus, according to the C4 final SC verdict in the docs, the issue should deserve a partial-grading, while being marked as a dup of #380 due to the root cause being the same.

Thank you for taking the time to read this.

#9 - alex-ppg

2023-12-09T19:25:05Z

Hey @mcgrathcoutinho, thanks for contributing! The root cause is not the presence of the circulating supply, it is the incorrect price calculation of a sale model 3. While it may make sense to carry over sale model 3 sales across different periods, it does not make sense to carry over mint restrictions. For example:

  • Collection A runs a periodic sale, amassing 10 NFTs sold, and reaching a price of 10 ETH per NFT
  • Collection A performs an airdrop as part of an incentive program
  • Collection A opens up the periodic sale for a public run, wanting to maintain the 10 ETH per NFT price

To fix this:

  • #381 would need to update its price calculation to use solely the circulating supply minted via the periodic sale.
  • #380 would need to reset its lastMintDate entirely

A fix for #380 does not fix #381 and vice versa. Both concern different parts of the code and require different alleviations. Arguing that #381 is not possible due to #380 existing in the codebase is not valid reasoning; each submission is treated as a "black box" independently assuming that all other code works as intended.

Based on the above, I will maintain these submissions as separate.

Findings Information

🌟 Selected for report: 0x3b

Also found by: KupiaSec, REKCAH, ZdravkoHr, degensec, dimulski, t0x1c

Labels

bug
2 (Med Risk)
primary issue
satisfactory
selected for report
edited-by-warden
M-09

Awards

1040.8142 USDC - $1,040.81

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/MinterContract.sol#L553

Vulnerability details

Impact

getPrice in salesOption 2 can round down to the lower barrier, effectively skipping the last time period. In simpler terms, if the price is scheduled to decrease over 4 hours, it will decrease for 2.999... hours and then jump to the price at the end of the 4th hour.

Proof of Concept

getPrice uses this else-if calculation to determine whether it should return the price as an equation or just the final price.

if (((collectionMintCost - collectionEndMintCost) / rate) > tDiff) {
    price = collectionPhases[_collectionId].collectionMintCost - (tDiff * collectionPhases[_collectionId].rate);
} else {
    price = collectionPhases[_collectionId].collectionEndMintCost;
}

The if statement's method of division by rate is incorrect, as it results in rounding down and incorrect price movements. This can cause the price to jump over the last time period, effectively decreasing faster than intended.

Example:

Values
collectionMintCost0.49 ETH
collectionEndMintCost0.1 ETH
rate0.1 ETH (per hour)
timePeriod1 hour - 3600 sec

Hour 0 to 1 - 0.49 ETH Hour 1 to 2 - 0.39 ETH - as it crosses the 1st hour, the price becomes 0.39 ETH Hour 2 to 3 - 0.29 ETH - same here It should go from 3 to 4 to 0.19 ETH, but that step is missed, and it goes directly to 0.1 ETH - the final price. Hour 3 to 4 - 0.10 ETH - the final price

Math

The equation for calculating tDiff using the provided variables is:

tDiff=block.timestamp−collectionPhases[collectionId].allowlistStartTimecollectionPhases[collectionId].timePeriodtDiff = \frac{{\text{block.timestamp} - \text{collectionPhases}[\text{collectionId}].\text{allowlistStartTime}}}{{\text{collectionPhases}[\text{collectionId}].\text{timePeriod}}}

We want the crossover when hour 2 switches to 3, which is when (block.timestamp - collectionPhases[_collectionId].allowlistStartTime) = 3 h or 10800 sec.

tDiff=108003600=3tDiff = \frac{10800}{3600} = 3

We plug in tDiff and the other variables into the if.

if (((collectionPhases[_collectionId].collectionMintCost - collectionPhases[_collectionId].collectionEndMintCost) 
                / (collectionPhases[_collectionId].rate)) > tDiff) {
0.49×1018−0.1×10180.1×1018=0.39×10180.1×1018=3\frac{0.49 \times 10^{18} - 0.1 \times 10^{18}}{0.1 \times 10^{18}} = \frac{0.39 \times 10^{18}}{0.1 \times 10^{18}} = 3

We obtain 3 as the answer, which is due to rounding down, as we divide 0.39 by 0.1. Solidity only works with whole numbers, causing the if to fail as 3 > 3 is false. This leads to entering the else statement where the last price of 0.1 ETH is returned, effectively missing one step of the 4-hour decrease.

PoC

Gist - https://gist.github.com/0x3b33/ab8a384f9979c4a9b7c4777be00a78de. Add contracts/=smart-contracts/ to mappings and run it with forge test --match-test test_changeProof --lib-paths ../smart-contracts -vv.

Logs:

[PASS] test_changeProof() (gas: 745699) Logs: 4900000000000000000 3900000000000000000 2900000000000000000 1000000000000000000

Tools Used

Manual review.

A simple yet effective suggestion is to add = in the if.

-  if (...) > tDiff){ 
+  if (...) >= tDiff){ 

Assessed type

Error

#0 - c4-pre-sort

2023-11-20T14:33:31Z

141345 marked the issue as duplicate of #393

#1 - c4-judge

2023-12-05T23:30:04Z

alex-ppg marked the issue as selected for report

#2 - alex-ppg

2023-12-05T23:37:41Z

The Warden illustrates that a rounding error can result in a price that is lower than the actual one for a particular period.

The relevant rounding issue has been detected by the bot in L-17, however, it is designated as a low-risk finding and the attack path that the Warden illustrates is not immediately clear by consuming the bot report submission.

The C4 Supreme Court verdict did not finalize on issues based entirely on bot findings and as such, I will deem this exhibit a valid medium-severity submission given that:

  • It takes a low-risk bot report and illustrates that it should be treated as a medium-severity issue that is not immediately discernible by consuming the bot report alone.

  • The bot report's mitigation would not lead to the loss of precision being resolved as it states that:

    ...it's recommended to require a minimum numerator amount to ensure that it is always greater than the denominator

The Warden's submission was selected as the best due to their clear depiction of the issue using mathematical formulas, a provision of both textual and coded PoCs, and a simple remediation path.

#3 - c4-judge

2023-12-05T23:37:47Z

alex-ppg marked the issue as satisfactory

Awards

10.9728 USDC - $10.97

Labels

bug
2 (Med Risk)
satisfactory
duplicate-175

External Links

Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/AuctionDemo.sol#L58

Vulnerability details

Impact

claimAuction and participateToAuction both use <=, which can result in bidders accidentally bidding immediately after the auction ends. This can lead to their bids becoming stuck in the contract.

function participateToAuction(...) public {...
require(block.timestamp <= minter.getAuctionEndTime(_tokenid)...);

function claimAuction(...) public WinnerOrAdminRequired(_tokenid,this.claimAuction.selector){
require(block.timestamp >= minter.getAuctionEndTime(_tokenid)...);

Proof of Concept

Shortly after the winner of an auction ends it by calling claimAuction, a bidder's transaction can execute and bid for the just-ended auction. This is possible because bidders can bid at the exact closing time, which should not be possible.

function participateToAuction(uint256 _tokenid) public payable {
    require(.. && block.timestamp <= minter.getAuctionEndTime(_tokenid) && ...);

Example scenario:

  1. Bob sends a bid a few seconds before the closing time.
  2. The bid transaction remains in the mempool.
  3. The winner closes the auction.
  4. Right after claimAuction, Bob's participateToAuction gets executed.

The winner (which should have been the last bidder - Bob, but it's the guy before him) receives his NFT, and all of the users get refunded on their bids, except for Bob. His bid remains stuck in the contract.

Tools Used

Manual review

Change from <= to < in participateToAuction to make it impossible for bidders to bid after an auction has ended.

function participateToAuction(uint256 _tokenid) public payable {
-   require(msg.value > returnHighestBid(_tokenid) && block.timestamp <= minter.getAuctionEndTime(_tokenid) && minter.getAuctionStatus(_tokenid) == true);
+   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);
}

Assessed type

Error

#0 - c4-pre-sort

2023-11-15T10:44:50Z

141345 marked the issue as duplicate of #962

#1 - c4-judge

2023-12-02T15:33:43Z

alex-ppg marked the issue as not a duplicate

#2 - c4-judge

2023-12-02T15:36:00Z

alex-ppg marked the issue as duplicate of #1926

#3 - c4-judge

2023-12-08T18:53:28Z

alex-ppg marked the issue as satisfactory

Awards

13.3948 USDC - $13.39

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
edited-by-warden
Q-28

External Links

IssueDescription
[L-01]Changing gencore will effect the mints
[L-02]getPrice uses different time checks from both mint and burnToMint
[L-03]People can set their hash to 0x00
[L-04]If randomizer is changed while a mint is happening token hash will be 0
[L-05]Emergency functions should not perform external calls
[L-06]Users can mint a few NFTs at once with sales option 3

[L-01] Changing gencore will effect the mints

Admins are able to change the gencore used by the minter with the help of updateCoreContract. However such change will effect any minting that are held, as the information inside gencore (whitelists, mint counts, cirSupply and so on) is used in the minting process.

Example:

  1. Mint on sales option 3 happens. Start price of 1 ETH, 100 minted with 0.01 ETN increase => 2ETH current price.
  2. Admin changes gencore
  3. The same NFT is scheduled for another sales option 3.
  4. It's price will start again from 1ETH , as gencore.viewCirSupply(_collectionId) will return 0.

[L-02] getPrice uses different time checks from both mint and burnToMint

While mint and burnToMint use >= and <= phase 2 under getPrice uses only > and <.

        } else if (
            collectionPhases[_collectionId].salesOption == 2
                && block.timestamp > collectionPhases[_collectionId].allowlistStartTime 
                && block.timestamp < collectionPhases[_collectionId].publicEndTime
        ) {

This will lead to a few inconsistency. Example is when users try and mint at block.timestamp == publicEndTime Which is possible in both mint and burnToMint, however instead of giving the decreased price,it will go into the else and return the normal collectionMintCost

[L-03] People can set their hash to 0x00

When minting tokens _mintProcessing which calls randomizer.calculateTokenHash(...), afterwards the randomizer calls setTokenHash to set the hash for this ID . When ChainLink is used as the randomizer the call is returned after a few block. This call can be made to revert on the return call, by simple providing enough gas for the request to reach ChainLink, but not enough for the return call to be successful. If the return call runs out of gas setTokenHash will never be called, thus leaving tokenToHash[tokenID] == 0. This will not cause any damage to the protocol, but may render some NFTs useless, or the opposite - extremely rare, due to their nature.

[L-04] If randomizer is changed while a mint is happening token hash will be 0

addRandomizer is used to change the randomizer for its collection. This function can be called at any time by an admin of this collection. When it is called, a new randomizer will be put in place. However, if a call is made to the old randomizer, its return function setTokenHash will take some time to execute. This means that the NFTs that were minted just before the change will have a hash of 0, rendering them useless.

Example:

  1. A user mints an NFT - _mintProcessing calls randomizer.calculateTokenHash, and VRF will take 3 blocks to respond.
  2. An admin changes the randomizer.
  3. Chainlink responds; however, setTokenHash reverts, as Chainlink is not the current randomizer.

The NFTs will have a hash of 0.

[L-05] Emergency functions should not perform external calls

emergencyWithdraw as the name suggest does a emergent withdraw. Emergent means that most likely a hack has happened and the protocol is looking to rescue the funds. However the function calls adminsContract.owner() which in direct contradiction with the emergency state. This may cause an issue if adminsContract is compromised. I suggest to have this address in storage or to be an input to the function.

[L-06] Users can mint a few NFTs at once with sales option 3

When using sales option 3, mint sets lastMintDate and timeOfLastMint. However it does not set them to the current mint time, but to the expected unlock time, unlike the docs periodic Sale Example scheme.

Example: Mint with 10 min time between mints.

  1. Mint starts at 24/07/2023 14:00
  2. First user mints at 24/07/2023 14:03
  3. The second user is able to mint at 24/07/2023 14:10

#0 - 141345

2023-11-25T08:15:52Z

#1 - c4-pre-sort

2023-11-25T08:20:31Z

141345 marked the issue as sufficient quality report

#2 - alex-ppg

2023-12-08T15:38:24Z

QA Judgment

The Warden's QA report has been graded B based on a score of 20 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:

Low-Risk

  • [L-02]: #1275
  • [L-06]: #1148

#3 - c4-judge

2023-12-08T15:38:31Z

alex-ppg marked the issue as grade-b

#4 - 0x3b33

2023-12-09T12:23:08Z

Hey, L-06 is a duplicate of 1148, and I believe its severity should not be 'L' but marked as 'M' and marked as dupe. The same should be done with L-02, as it also points to the main root cause and is explained good enough, although shortly. The only issue is that I have misjudged the severity of those findings, I didn't submit them as medium in order to not spam the contest with inflated findings. Generally it is recommended that hard to qualify finding should be submitted as LOWs and if it's a MED the judge will raise it to it's appropriate severity.

#5 - alex-ppg

2023-12-09T14:28:20Z

Hey @0x3b33, thanks for contributing! Submission #1148 ended up being nullified in the PJQA process.

Concerning #1275, I do not deem this submission to be of sufficient quality to be eligible for a portion of its rewards. #1275 and its duplicates have a detailed PoC, elaborate on what the impact is, how it should be corrected, etc. In the interest of fairness, I cannot provide this QA submission a reward as its comparative quality is very low and it would incentivize giant QA reports to be submitted that detail all issues and leave the burden of upgrading to the judges which is unfair from a contest's perspective.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter