xanoutas/solidity-security-findings huggingface dataset:
Switching to sales model 3 for a collection with pre-existing supply could brick the ' mint() ' function for that collection.
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

' Periodic sales' model cannot be used in a collection with  Pre-existing supply
because of how ` timeOfLastMint ` is calculated in `mint()` function.

This prevents the system from flexibly combining different sales models in a single collection 
as intended by the business model. 


## Proof of Concept

https://seize-io.gitbook.io/nextgen/nextgen-smart-contracts/features#minting-process

The docs of the NextGen Protocol (linked above) states the following, 
  
```

* A collection can have an arbitrary number of phases (starting and ending times), 
each with their own allowlist.

* The sales models can be combined with phased allowlists to execute very complex drops.

```
Lets consider a common scenario followed in the NFT community, where 
Total Supply = 8500
collectionMintCost = 1 ETH 
100 % of the total supply is allocate to allowlisted addresses, and 
any unminted NFTs go over to public sale phase.

Lets say 8000 NFTs are minted in allowlist phase.

The collection's admin wants to utilize ' Periodic sales' (sales model 3)  for unminted tokens in a public sale phase.

In this case, 
the `lastMintDate[col]` would be assigned a value that is much higher due to ` gencore.viewCirSupply(col) ` in the formula

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

```

This means ` timeOfLastMint ` will be much greater than ` block.timestamp ` and tDiff will be 0.

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

This effectively reverts due to the following check,

```
require(tDiff>=1 && _numberOfTokens == 1, "1 mint/period");
```

preventing the NextGen system from flexibly combining different sales models in a single collection 
as intended by the business model. 


## Tools Used

Manual Analysis, Foundry.

## Recommended Mitigation Steps

Remodel the way to calculate ' lastMintDate[col] '  
by tracking the number of NFTs minted through `sales model 3` using a separate counter
without taking the collection's current ciculating supply into account.





## Assessed type

Math
Adversary can reenter `mint` to bypass max allowance.
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/NextGenCore.sol#L189-L200


# Vulnerability details

## Description 

`MinterContract.mint` calls `NextGenCore.mint`, which variables that accounts the amount of tokens each user minted is changed only after `_mintProcessing`, that has a callback in `_safeMint`. Because of that, attackers can reenter `MinterContract.mint` before `tokensMintedAllowlistAddress` and `tokensMintedPerAddress` are incremented, allowing them to bypass the max allowance check that limits the amount of tokens an user can mint for public or allowlist phase of a collection.

## Proof of Concept 

1. Let's supose the following values:
- `tokensMintedPerAddress[_collectionID][attacker] = 2` (retrieved via `retrieveTokensMintedPublicPerAddress`)
- `collectionAdditionalData[_collectionID].maxCollectionPurchases = 3` (retrieved via `viewMaxAllowance`)

2. Adversary calls `MinterContract.mint` to mint one token. Logic that executes for public phase (and no delegation, sales mode not 3):
```solidity
        } 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);
        }
```
- The following line checks if user didn't overpass the limit of mints per collection, comparing `tokensMintedPerAddress[_collectionID][attacker]` plus amount of tokens to mint and `collectionAdditionalData[_collectionID].maxCollectionPurchases`. 
```solidity
require(gencore.retrieveTokensMintedPublicPerAddress(col, msg.sender) + _numberOfTokens <= gencore.viewMaxAllowance(col), "Max");
```
- This is `3 <= 3`, which is true, so no reverts. 
- Then, subcall `gencore.mint` is called: 
```solidity
    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].collectionCi
`RandomizerNXT` allows randomness re-rolling and also front-running.
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/RandomizerNXT.sol#L55-L59


# Vulnerability details

## Description 

When a collection uses `RandomizerNXT` as the randomizer, the process of minting and setting the token hash happens in the same transaction and block, which allows two attacks. First, a user can see the randomness outcome in mempool and front-run his own transaction to revert in `_safeMint` callback if he doesn't like the token hash result. This means that who has access to the generative art algorithm can test the hashs and re-roll it via reverts if he wants, only paying gas each re-roll, until a hash that generates a art he enjoyed or that looks highly speculative rolls. Secondly, front-running to predict and steal a Token Hash is possible, because randomness is calculated using only `_mintIndex` as unique value, allowing attacker to get a `_mintIndex` that leads to same token hash before the front-runned user.

## Proof of Concept

1. User calls `mint` in a collection that uses `RandomizerNXT`. 
2. The subcall `_mintProcessing` executes:
```solidity
    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);

    }
```
3. The first important line in this subcall is a call to `RandomizerNXT.calculateTokenHash`: 
```solidity
    function calculateTokenHash(uint256 _collectionID, uint256 _mintIndex, uint256 _saltfun_o) public {
        require(msg.sender == gencore);
        bytes32 hash = keccak256(abi.encodePacked(_mintIndex, blockhash(block.number - 1), randoms.randomNumber(), randoms.randomWord()));
        gencoreContract.setTokenHash(_collectionID, _mintIndex, hash);
    }
```
- It calculates the random token hash in `bytes32 hash = keccak256(abi.encodePacked(_mintIndex, blockhash(block.number - 1), randoms.randomNumber(), randoms.randomWord()));`
- Then, it sets the token hash of the NFT in `gencoreContract.setTokenHash(_collectionID, _mintIndex, hash);`

4. After the token hash is calculated and set, the last line of `_mintProcessing` is `_safeMint(_recipient, _mintIndex);`, which triggers a callback in the ERC-721 Receiver: 
```solidity
    function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual {

        _mint(to, tokenId);

        require(

            _checkOnERC721Received(address(0), to, tokenId, data),

            "ERC721: transfer to non ERC721Receiver implementer"

        );

    }
```

5. The callback allows the re-roll exploit because:
- User can see the randomness outcome in the mempool and see which Token Hash the `RandomizerNXT` generated. 
- He tests the token hash in the gene
`claimAuction` can be reverted by any bidder, locking all funds and the prize.
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Description 

`claimAuction` is used to redeem the auction's ERC-721 and refund all bidders that didn't win the auction. In this process, callbacks are sent to every single bidder via low-level calls (that triggers fallbacks/receives) and `ERC721.safeTransferFrom`. So, every bidder has an access to a callback when `claimAuction` is called, which makes possible to any of them to perma-revert the function via gas-griefing.

## Proof of Concept 
1. Auction has ended and the winner calls `claimAuction`: 
```solidity
    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 {}
        }
    }
```

2. If an attacker is a bidder that lost the auction, he needs to be refunded. So, the following line will call him:
```
(bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");
```

3. If attacker is a smart contract, `fallback/receive` will be triggered to handle the received ether. In this callback, attacker can gas-grief returning a very long string:
```solidity
fallback() external payable {
	if (gasgrief) {
		return VERY_LONG_STRING; 
	}
}
```
- The caller doesn't assign a variable to the return value, but in solidity it is stored anyway in caller's memory when low-level call is used, which makes the gas-grief possible.

4. The gas-grief will revert `claimAuction`,  potentially perma-locking all the funds and the prize in the contract, because there are no other function to claim the refunds or the prize. Also, there is no emergency withdraw function.

5. If luckily there weren't any auction's loser trying to gas-grief, the winner can also denia
Re-enterancy in AuctionDemo contract
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
`claimAuction` function of `AuctionDemo` contract transfers the token to highest bidder (winner) and the bid amount is transferred to the owner. Moreover, refund is sent to all remaining participants (non-winners) of the auction via `call` function. However, if a non-winner is a contract, then it can invoke the `cancelBid` function when refund is sent to it. This way, the attacker can get the refund twice; once by the `claimAuction` function and secondly by `cancelBid` function.

## Proof of Concept
1. Attacker places a bid and tries to not become the winner.
2. Winner or Admin calls `claimAuction` function exactly on the auction end time (i.e block.timestamp == AuctionEndTime), this causes the following condition to become true :-
`require(block.timestamp >= minter.getAuctionEndTime(_tokenid)...`
3. Refund is sent to attacker via `call` and the attacker invokes the `cancelBid` function and his bid is cancelled as following condition is also still true :-

`function cancelBid(uint256 _tokenid, uint256 index) public {
     require(block.timestamp <= minter.getAuctionEndTime(_tokenid), "Auction ended");`

4. Attacker's bid gets cancelled and his amount is again refunded to him at line 128
`(bool success, ) = payable(auctionInfoData[_tokenid][index].bidder).call{value: auctionInfoData[_tokenid][index].bid}("");`
5. This allows attacker to get his bid amount refunded twice.

Same is also 

## Tools Used
Manual Review

## Recommended Mitigation Steps
Line 115 should change the status to false before sending refund
`} else if (auctionInfoData[_tokenid][i].status == true) {`

Moreover, remove equality from the condition in `cancelBid` function 
`require(block.timestamp < minter.getAuctionEndTime(_tokenid), "Auction ended");`
or remove equality from the condition in `claimAuction` function 
`require(block.timestamp > minter.getAuctionEndTime(_tokenid)...`





## Assessed type

Reentrancy
Risk of Permanent ETH Loss for Bidders
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/AuctionDemo.sol#L116-L117


# Vulnerability details

## Impact
There's a risk of ETH becoming irretrievably locked in the contract if a bidder's address is a contract with either complex logic in its receive() function or no receive() function at all. In such cases, ETH transfers to these addresses could fail and result in permanent loss.

## Proof of Concept
-The claimAuction function iterates through auction participants, attempting to return their ETH bids. However, it fails to verify the success of these transactions. 
This is evident in the line:
`(bool success, ) = payable(auctionInfoData[_tokenid][i].bidder).call{value: auctionInfoData[_tokenid][i].bid}("");`, where success is not checked.
-The contract currently lacks mechanisms to handle failed ETH transfers. As a result, a bidder's funds can be lost indefinitely if the transfer to their contract address fails.

## Tools Used
 manual review

## Recommended Mitigation Steps
- Implement checks to verify the success of ETH transfers. If a transfer fails, the contract should have a mechanism to handle the situation, such as allowing bidders to manually withdraw their funds.



## Assessed type

ETH-Transfer
Contract can be drained from the `claimAuction` contract
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
Due to the way the `claimAuction` function is written, it is also possible to drain the contract when sending bids back to users. This can only happen if the highestBidder made multiple bids which is likely.

## Proof of Concept
In this section of the `claimAuction` function
```solidity
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 {}
        }
```
This function `(bool success, ) = payable(owner()).call{value: highestBid}("");` specifically transfers the `highestBid` every time the highestBidder bids is called in the loop.

So for a case where the highestBidder bids 0.5Eth, 0.7Eth, 0.8Eth, 1.2Eth bids and 1.5Eth as the winning bid for the auction, the `claimAuction` function will transfer our 1.5 * 5 out of the contract. 

In a best-case scenario, the contract is drained to the owner wallet, and in the worst-case scenario it'll be impossible to claim the auction NFT in a case where total of Eth left is not up to 7.5Eth, 


## Tools Used
Manual Review

## Recommended Mitigation Steps
- check for each bids and send the amount of each bids just as done in the else if statement


## Assessed type

ETH-Transfer
Users get pay for multiple NFTs and only get 1 minted 
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L197
https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L233


# Vulnerability details

## Impact
Users get only 1 NFT after paying for multiple or more than one 

## Proof of Concept
In MinterContract.mint users can mint more than one token and are allowed to pay for the number of tokens minted.
 
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");
.....

 require(msg.value >= (getPrice(col) * _numberOfTokens), "Wrong ETH");

payment for the number of tokens inputed is done here; then mint is called in Nextgencore

     gencore.viewCirSupply(col);
            gencore.mint(
                mintIndex,
                mintingAddress,
                _mintTo,
                tokData,
                _saltfun_o,
                col,
                phase
            );

then in mint the address is updated by adding just one token 

{
            _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;
            }
        }
    }

 _mintProcessing() is then called which further calls openzeppelin  ERC721 _safemint() and in the end only one token is minted and updated but the user could have paid for more than one. 
## Tools Used
Manual Review
## Recommended Mitigation Steps
Get the numberOftokens minted input from the mintercontract and use it when updating the  tokensMintedPerAddress in the Nextgencore.


## Assessed type

Other
mint with sales option 3 doesn't work as expected
Audit: 2023-10-nextgen-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Summary

`lastMintDate[col]` is set to be higher than expected, which can lead to the minting process being blocked for some time. 

## Vulnerability Details

In the `mint` function in the `MintContract` if the `salesOption` of the collection is set to 3 then we can only mint 1 token per period. This is the expression used to calculate when the `lastMintDate[col]` was:

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

However if we airdrop tokens the `circulatingSupply` increases and so does the `lastMintDate`. So now if 1 time period has passed and someone tries to mint, he won't be able to do it.

## Proof of Concept

Add this test to `nextGen.test.js` and run `npx hardhat test`, also at the top of the test file add: (`const {
  loadFixture,
  time
} = require("@nomicfoundation/hardhat-toolbox/network-helpers")`)

    context("Custom tests", () => {

      it("#DoS salesoption 3 with airdrop", async function () {
        await contracts.hhCore.createCollection(
          "Test Collection 5",
          "Artist 5",
          "For testing",
          "www.test.com",
          "CCO",
          "https://ipfs.io/ipfs/hash/",
          "",
          ["desc"],
        )

        await contracts.hhCore.setCollectionData(
          5, // _collectionID
          signers.addr1.address, // _collectionArtistAddress
          2, // _maxCollectionPurchases
          10000000, // _collectionTotalSupply 10 mil
          1000, // _setFinalSupplyTimeAfterMint
        )

        await contracts.hhCore.addRandomizer(
          5, contracts.hhRandomizer,
        )

        await contracts.hhMinter.setCollectionCosts(
          5, // _collectionID
          0, // _collectionMintCost
          0, // _collectionEndMintCost 0
          0, // _rate
          200, // _timePeriod
          3, // _salesOptions
          '0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B', // delAddress
        )

        await contracts.hhMinter.setCollectionPhases(
          5, // _collectionID
          1699993193, // _allowlistStartTime
          1699993193, // _allowlistEndTime
          1699993193, // _publicStartTime
          1796931278, // _publicEndTime
          "0x8e3c1713145650ce646f7eccd42c4541ecee8f07040fc1ac36fe071bbfebb870", // _merkleRoot
        )
      
        // airdrop some tokens
        await contracts.hhMinter.airDropTokens([signers.addr3.address], ["someTokenData"], [28], 5, [100]);
      
        await time.setNextBlockTimestamp(1699993193 + 1);
      
        //random user trying to mint
        await contracts.hhMinter.mint(5, 1, 1, '{"tdh": "100"}', signers.addr2.address,
           ["0x0000000000000000000000000000000000000000000000000000000000000000"], 
             "0x0000000000000000000000000000000000000000", 235);