Reentrancy in mint function leads to various problems
Closed this issue · 8 comments
Lines of code
Mint function in minter contract: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/MinterContract.sol#L196-L254
Mint function in core contract: https://github.com/code-423n4/2023-10-nextgen/blob/8b518196629faa37eae39736837b24926fd3c07c/smart-contracts/NextGenCore.sol#L189-L200
Vulnerability details
Bug Description
When minting NFTs, users will using the mint
function. This function will mint a NFT using the _safeMint
function. The problem is that this mint will be done before crucial variables are updated.
Mint before checking the salesOption == 3
conditions are fulfilled in MinterContract.sol
:
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) {
uint timeOfLastMint;
if (lastMintDate[col] == 0) {
// for public sale set the allowlist the same time as publicsale
timeOfLastMint = collectionPhases[col].allowlistStartTime - collectionPhases[col].timePeriod;
} else {
timeOfLastMint = lastMintDate[col];
}
// uint calculates if period has passed in order to allow minting
uint tDiff = (block.timestamp - timeOfLastMint) / collectionPhases[col].timePeriod;
// users are able to mint after a day passes
require(tDiff>=1 && _numberOfTokens == 1, "1 mint/period");
lastMintDate[col] = collectionPhases[col].allowlistStartTime + (collectionPhases[col].timePeriod * (gencore.viewCirSupply(col) - 1));
}
Mint before updating tokensMintedAllowlistAddress
and tokensMintedPerAddress
in NextGenCore.sol
mint
.
_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;
}
The vulnerability arises during the execution of the _safeMint
function, which involves the usage of _checkOnERC721Received
. Exploiting this, attackers can repeatedly mint NFTs by employing a smart contract that reenters the function upon receiving the call of _checkOnERC721Received
. The minting process occurs prior to updating crucial variables such as tokensMintedAllowlistAddress
and tokensMintedPerAddress
, as well as implementing the control mechanism for sale option 3. This oversight allows attackers to mint the entire collection without proper limitations.
Impact
The reentrancy vulnerability exposes the system to significant risks, allowing attackers to exploit the following critical issues:
- Bypassing the mint amount limit within the allowlist (
tokensMintedAllowlistAddress
) - Bypassing the mint amount limit during public sales (
tokensMintedPerAddress
) - Bypassing the control mechanism for sale option 3 (
salesOption == 3
), enabling unlimited NFT minting within a specific timeframe
In some cases, attackers can also drain protocol's funds.
Proof of Concept
Example Illustrating Potential Fund Loss for the Protocol
Scenario
- A collection offers a 0 ETH minting price during the allowlist period.
- The collection incorporates Chainlink's VRF, invoking the
calculateTokenHash
function inRandomizerVRF.sol
during NFT minting. This involves the usage of Link tokens to generate randomness. - An attacker is permitted to mint only 1 token within the allowlist period.
Attack Path
- The attacker deploys a malicious smart contract that implements IERC721Receiver, initiating reentry into the
mint
function upon NFT receipt using theonERC721Received
function. - The attacker triggers the
mint
function using the malicious smart contract. - The smart contract repeatedly reenters the
mint
function throughonERC721Received
. - Since
tokensMintedAllowlistAddress
remains unaltered, the mint amount for the allowlist check is bypassed. - The malicious contract successfully mints the entire collection.
This vulnerability results in the attacker being able to mint the whole collection for free and the protocol suffering substantial financial losses, considering the requirement for a significant number of Link tokens to generate the unique hash for all the NFTs. While potential gas limit issues might arise, this scenario effectively demonstrates the concept. Additionally, this vulnerability can be exploited to bypass the mint amount limits during public sales and the control mechanism for sale option 3.
Tools Used
Manual review
Recommended Mitigation Steps
To address this vulnerability, it is imperative to update tokensMintedAllowlistAddress
and tokensMintedPerAddress
before executing the _mintProcessing
function within NextGenCore.sol
's mint
function. Furthermore, implement the control mechanism for sale option 3 in the MinterContract.sol
before invoking the mint
function in NextGenCore.sol
.
Assessed type
Reentrancy
Issue created on behalf of judge in order to split into 2 findings
alex-ppg marked the issue as selected for report
alex-ppg marked the issue as satisfactory
This submission was created as part of a divergence of #1517 per the relevant comment.
Identically to the original exhibit, this submission has been adequately graded as high due to breaking a main invariant of the protocol and with a sensitive sale model at that (periodic sales which permit one NFT per period).
After further consideration, the submission's rationale concerning bypassing the minting limit is incorrect. The code adheres to an Interactions-Checks-Effects pattern, rendering it secure against a re-entrancy that attempts to bypass the limit itself.
A real vulnerability that arises from this is re-using the same getPrice
for a valid multi-token period mint in case multiple periods have elapsed with no mint. As such, I will invalidate this and all relevant submissions as none identified this particular exploitation vector.
alex-ppg marked the issue as unsatisfactory:
Invalid
Upon further consideration, the getPrice
attack vector is not feasible either. The NextGenCore::mint
function will increment the circulating supply prior to minting, causing price measurements to be correct. As such, no attack vector presently exists from a re-entrancy arising from periodic sales.