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
User calls mint in a collection that uses RandomizerNXT.
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);
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:
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 generative art algorithm and see if he liked or if looks trendy and speculative.
- If he didn't like the token hash, he can front-run his own transaction to make `onERC721Received` revert it:
```solidity
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4){
if (gasgrief) {
revert();
}
}
So, user reverted the transaction paying only the gas cost, and now can call mint again to test another Token Hash. He will do this until a token hash that generates the art he wants rolls. In the other side, a honest user needs to pay the NFT's full price for each roll.
Additionally to this exploit, user can check in the mempool the token hash that another user generated. If he did the same process of testing and liked the token hash outcome, a front-run to steal that token hash is possible:
As you can see in the process of randomness generation, the only unique variable to generate it is _mintIndex:
- So, attacker can front-run a transaction to mint a NFT of a specific `_mintIndex` first than the front-runned transaction. As the other variables to calculate the randomness are `block.number`, `block.timestamp`, `blockhash`, `block.prevrandao`, effectivelly having the same `_mintIndex` in the same block leads to the same randomness and also makes the targeted user to generate a different token hash.
## Impact
If collection has a website or app to test the token hashs, user can re-roll paying only the gas cost until he rolls an art that looks highly speculative. This is a completely unfair use of randomness, specially for art which has highly speculative values.
Even Chainlink's VRF is against re-rolling (https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness). It's a problem because while a user that is unaware of the attack only has one roll for each NFT, an attacker has infinite chances, which also can be used for profit.
If only a set of restricted people can test the token hashs in the generative art algorithm, it's also unfair and the vulnerability will make people don't trust the collection.
Addionality, the second exploit presented allows attackers to predict and steal other people hashs via frontruns.
Likelihood: High.
Severity: High.
Risk: High
Tools Used
Manual Review
Recommended Mitigation Steps
Make the mint/randomness process a 2-step mechanism. In other words, don't let minting and setting the token hash to happen in the same transaction. This will completely solve the re-roll exploit. Also, the second exploit of front-running _mintIndex to predict and steal a token hash of the mempool will be also solved, because when randomness comes the _mintIndex will be already minted.
The randomness step of the 2-step mechanism must have a block check, because if it happens in the same block as the minting step the exploits will still be possible, just harder.
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
mint
in a collection that usesRandomizerNXT
._mintProcessing
executes:tokenData[_mintIndex] = _tokenData;
collectionAdditionalData[_collectionID].randomizer.calculateTokenHash(_collectionID, _mintIndex, _saltfun_o);
tokenIdsToCollectionIds[_mintIndex] = _collectionID;
_safeMint(_recipient, _mintIndex);
}
bytes32 hash = keccak256(abi.encodePacked(_mintIndex, blockhash(block.number - 1), randoms.randomNumber(), randoms.randomWord()));
gencoreContract.setTokenHash(_collectionID, _mintIndex, hash);
_mintProcessing
is_safeMint(_recipient, _mintIndex);
, which triggers a callback in the ERC-721 Receiver:_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data),
"ERC721: transfer to non ERC721Receiver implementer"
);
}
So, user reverted the transaction paying only the gas cost, and now can call
mint
again to test another Token Hash. He will do this until a token hash that generates the art he wants rolls. In the other side, a honest user needs to pay the NFT's full price for each roll.Additionally to this exploit, user can check in the mempool the token hash that another user generated. If he did the same process of testing and liked the token hash outcome, a front-run to steal that token hash is possible:
_mintIndex
:function randomNumber() public view returns (uint256){ uint256 randomNum = uint(keccak256(abi.encodePacked(block.prevrandao, blockhash(block.number - 1), block.timestamp))) % 1000; return randomNum; }
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); }
If collection has a website or app to test the token hashs, user can re-roll paying only the gas cost until he rolls an art that looks highly speculative. This is a completely unfair use of randomness, specially for art which has highly speculative values. Even Chainlink's VRF is against re-rolling (https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness). It's a problem because while a user that is unaware of the attack only has one roll for each NFT, an attacker has infinite chances, which also can be used for profit. If only a set of restricted people can test the token hashs in the generative art algorithm, it's also unfair and the vulnerability will make people don't trust the collection. Addionality, the second exploit presented allows attackers to predict and steal other people hashs via frontruns.
Tools Used
Manual Review
Recommended Mitigation Steps
_mintIndex
to predict and steal a token hash of the mempool will be also solved, because when randomness comes the_mintIndex
will be already minted.Assessed type
MEV