RandomizerNXT produces entirely predictable values, rendering its use for generating random values in generative art NFTs counterproductive and potentially harmful. This flaw allows attackers to exploit the predictability to mint NFTs when the expected perceived value of the generated art is highest.
While perceived value is subjective, certain artist choices can make it more objective and easily quantifiable. For instance, an artist could design a collection with a generative script that imparts special and rare features (e.g., a golden frame, shining card) for a tokenHash with one or more leading zeroes or other characteristics.
The scope of influence that a tokenHash holds over the ultimate NFT value is as extensive as the spectrum of artistic possibilities. Therefore, it becomes crucial to generate fairly unpredictable values for the generative art scripts, that cannot be gameable or rejected. Failure to do so not only enables attackers to exploit the system unfairly for acquiring more valuable NFTs but also risks undermining the overall value of the entire collection, impacting other investors adversely. Avoiding service fees is not worth the risks associated with generating predictable or gameable values for generative art NFTs.
Proof of Concept
According to documentation, RandomizerNXT is:
A custom-made implementation Randomizer contract that uses the token id, the blockchash of the previous block and a random pool of words and numbers to generate the tokenHash.
Once the calculateTokenHash(..) function is called it triggers a call to the word pool contract to get a random word and a random number that are used along with the other parameters to calculate the random hash. Once the hash is calculated, it calls the setTokenHash(..) function on the Core contract to store the hash.
However, there can be no meaningfully random pool of words and numbers generated from pre-existing, known values.
In the RandomizerNXT contract, the calculateTokenHash function produces a hash through the following process:
_mintIndex and blockhash(block.number - 1) are entirely predictable. This is the underlying mechanism behind the randoms.randomNumber and randoms.randomWord functions:
function randomNumber() public view returns (uint256){
uint256 randomNum = uint(keccak256(abi.encodePacked(block.prevrandao, blockhash(block.number - 1), block.timestamp))) % 1000;
return randomNum;
}
function randomWord() public view returns (string memory) {
uint256 randomNum = uint(keccak256(abi.encodePacked(block.prevrandao, blockhash(block.number - 1), block.timestamp))) % 100;
return getWord(randomNum);
}
The same predictability extends to block.prevrandao and block.timestamp. The getWord function contributes no unpredictability; it solely uses randomNum to retrieve words from a predetermined list.
As evident, the predictability of the hashes generated by RandomizerNXT is established.
Demonstration
For clarity, let us perform a quick test in the codebase.
Create a new file at hardhat/smart-contracts/audit/PredictablyRandomPoC.sol and add the following content:
Next, since we are using .only to only run our test, execute the following command from within the hardhat directory:
$ npx hardhat test
In the context of this Proof of Concept (PoC), the test merely showcases the predictability of the value re-using the randomPool contract code. However, it's worth noting that the actual prediction could easily be made off-chain and involve a very sophisticated approach for tokenHash selection.
Tools Used
Manual: code editor, Hardhat.
Recommended Mitigation Steps
Opt for randomizers with reliably unpredictable randomness, such as NextGenRandomizerVRF and NextGenRandomizerRNG. Or build one with an equivalent design.
Lines of code
https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/RandomizerNXT.sol#L57 https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/XRandoms.sol#L35-L43
Vulnerability details
Impact
RandomizerNXT
produces entirely predictable values, rendering its use for generating random values in generative art NFTs counterproductive and potentially harmful. This flaw allows attackers to exploit the predictability to mint NFTs when the expected perceived value of the generated art is highest.While perceived value is subjective, certain artist choices can make it more objective and easily quantifiable. For instance, an artist could design a collection with a generative script that imparts special and rare features (e.g., a golden frame, shining card) for a
tokenHash
with one or more leading zeroes or other characteristics.The scope of influence that a
tokenHash
holds over the ultimate NFT value is as extensive as the spectrum of artistic possibilities. Therefore, it becomes crucial to generate fairly unpredictable values for the generative art scripts, that cannot be gameable or rejected. Failure to do so not only enables attackers to exploit the system unfairly for acquiring more valuable NFTs but also risks undermining the overall value of the entire collection, impacting other investors adversely. Avoiding service fees is not worth the risks associated with generating predictable or gameable values for generative art NFTs.Proof of Concept
According to documentation,
RandomizerNXT
is:However, there can be no meaningfully random pool of words and numbers generated from pre-existing, known values.
In the
RandomizerNXT
contract, thecalculateTokenHash
function produces a hash through the following process:https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/RandomizerNXT.sol#L57
_mintIndex
andblockhash(block.number - 1)
are entirely predictable. This is the underlying mechanism behind therandoms.randomNumber
andrandoms.randomWord
functions:https://github.com/code-423n4/2023-10-nextgen/blob/main/smart-contracts/XRandoms.sol#L35-L43
The same predictability extends to
block.prevrandao
andblock.timestamp
. ThegetWord
function contributes no unpredictability; it solely usesrandomNum
to retrieve words from a predetermined list.As evident, the predictability of the hashes generated by
RandomizerNXT
is established.Demonstration
For clarity, let us perform a quick test in the codebase.
Create a new file at
hardhat/smart-contracts/audit/PredictablyRandomPoC.sol
and add the following content:Additionally, create the Hardhat test file at
hardhat/test/predictablyRandom.js
with the following content:Next, since we are using
.only
to only run our test, execute the following command from within thehardhat
directory:In the context of this Proof of Concept (PoC), the test merely showcases the predictability of the value re-using the
randomPool
contract code. However, it's worth noting that the actual prediction could easily be made off-chain and involve a very sophisticated approach fortokenHash
selection.Tools Used
Manual: code editor, Hardhat.
Recommended Mitigation Steps
Opt for randomizers with reliably unpredictable randomness, such as
NextGenRandomizerVRF
andNextGenRandomizerRNG
. Or build one with an equivalent design.Assessed type
Other