Bypassing _maxAllowance in NextGenMinterContract.mint(): Enables minting more NFTs than permitted.
Exploiting reentrancy in NextGenMinterContract.burnToMint(): Allows acquiring both burnable and mintable NFTs at the same time.
Proof of Concept
The 1st case
The typical flow of a user mint is as follows:
User tx -> NextGenMinterContract.mint() external call -> NextGenCore.mint() internal call -> NextGenCore._mintProcessing()
The root of the problem lies in the NextGenCore.mint() and NextGenCore._mintProcessing() functions.
In the mint() function, the _mintProcessing() is called first and then the variables tokensMintedAllowlistAddress or tokensMintedPerAddress are updated. However, this creates a reentrancy issue because the _safeMint() at the end of _mintProcessing() calls _recipient.onERC721Received().
_recipient can be either the user or his chosen delegate, so passing a smart contract is not a problem.
To take advantage of the reentrancy, a user can pass a smart contract that will call the NextGenMinterContract.mint() function again as a delegate of the user. The function's checks will hold as the tokensMintedAllowlistAddress or tokensMintedPerAddress variables are still not updated. This method allows the user to mint as many tokens as the gas limit allows.
If the user is a smart contract, the execution is simplified as delegation is unnecessary.
The 2nd case
A malicious user can call NextGenMinterContract.burnToMint() from a smart contract. The flow is similar to the first case, where _mintProcessing() is called before burning the NFT, enabling the user to utilize both burnable and mintable NFTs together for potential profit extraction.
function burnToMint(
uint256 mintIndex,
uint256 _burnCollectionID,
uint256 _tokenId,
uint256 _mintCollectionID,
uint256 _saltfun_o,
address burner
) external {
require(msg.sender == minterContract, "Caller is not the Minter Contract");
require(_isApprovedOrOwner(burner, _tokenId), "ERC721: caller is not token owner or approved");
collectionAdditionalData[_mintCollectionID].collectionCirculationSupply =
collectionAdditionalData[_mintCollectionID].collectionCirculationSupply + 1;
if (
collectionAdditionalData[_mintCollectionID].collectionTotalSupply
>= collectionAdditionalData[_mintCollectionID].collectionCirculationSupply
) {
_mintProcessing(mintIndex, ownerOf(_tokenId), tokenData[_tokenId], _mintCollectionID, _saltfun_o);
// burn token
_burn(_tokenId);
burnAmount[_burnCollectionID] = burnAmount[_burnCollectionID] + 1;
}
}
As long as the user has the burnable NFT in the end of his actions, the transaction will succeed.
Tools Used
Manual review
Recommended Mitigation Steps
Add reentrancy checks to the NextGenMinterContract.mint() and NextGenMinterContract.burnToMint() functions such as ReentrancyGuard by OZ or change the function to mint only after all the storage changes had been made.
Lines of code
https://github.com/code-423n4/2023-10-nextgen/blob/2467db02cc446374ab9154dde98f7e931d71584d/smart-contracts/MinterContract.sol#L213 https://github.com/code-423n4/2023-10-nextgen/blob/2467db02cc446374ab9154dde98f7e931d71584d/smart-contracts/MinterContract.sol#L217 https://github.com/code-423n4/2023-10-nextgen/blob/2467db02cc446374ab9154dde98f7e931d71584d/smart-contracts/MinterContract.sol#L224
Vulnerability details
Impact
_maxAllowance
inNextGenMinterContract.mint()
: Enables minting more NFTs than permitted.NextGenMinterContract.burnToMint()
: Allows acquiring both burnable and mintable NFTs at the same time.Proof of Concept
The 1st case
The typical flow of a user mint is as follows: User tx ->
NextGenMinterContract.mint()
external call ->NextGenCore.mint()
internal call ->NextGenCore._mintProcessing()
The root of the problem lies in the
NextGenCore.mint()
andNextGenCore._mintProcessing()
functions.In the
mint()
function, the_mintProcessing()
is called first and then the variablestokensMintedAllowlistAddress
ortokensMintedPerAddress
are updated. However, this creates a reentrancy issue because the_safeMint()
at the end of_mintProcessing()
calls_recipient.onERC721Received()
._recipient
can be either the user or his chosen delegate, so passing a smart contract is not a problem.To take advantage of the reentrancy, a user can pass a smart contract that will call the
NextGenMinterContract.mint()
function again as a delegate of the user. The function's checks will hold as thetokensMintedAllowlistAddress
ortokensMintedPerAddress
variables are still not updated. This method allows the user to mint as many tokens as the gas limit allows.If the user is a smart contract, the execution is simplified as delegation is unnecessary.
The 2nd case
A malicious user can call
NextGenMinterContract.burnToMint()
from a smart contract. The flow is similar to the first case, where_mintProcessing()
is called before burning the NFT, enabling the user to utilize both burnable and mintable NFTs together for potential profit extraction.As long as the user has the burnable NFT in the end of his actions, the transaction will succeed.
Tools Used
Manual review
Recommended Mitigation Steps
Add reentrancy checks to the
NextGenMinterContract.mint()
andNextGenMinterContract.burnToMint()
functions such as ReentrancyGuard by OZ or change the function to mint only after all the storage changes had been made.MinterContract.sol
NextGenCore.sol
Assessed type
Reentrancy