Platform: Code4rena
Start Date: 06/05/2021
Pot Size: $66,000 USDC
Total HM: 16
Participants: 11
Period: 6 days
Judge: cemozer
Total Solo HM: 9
Id: 8
League: ETH
Rank: 5/11
Findings: 5
Award: $7,229.38
🌟 Selected for report: 2
🚀 Solo Findings: 1
gpersoon
In NFTXVaultUpgradeable.sol the received ERC1155 NFT's are stored in the array holdings. The number of NFT's of a specific type are kept in quantity1155. Suppose there is one NFT (#1) with 10 instances and 9 NFT's with 1 instance. (#2..#10) When you randomly retrieve a NFT via getRandomTokenIdFromFund it takes a random number from the holdings array, without taking in account the number of instances. So in the example the probability of getting a NFT of type #1 is about 10% While if you take in account all the NFT's + instances then the change should be more than 50%.
function getRandomTokenIdFromFund() internal virtual returns (uint256) { uint256 randomIndex = getPseudoRand(holdings.length()); return holdings.at(randomIndex); }
Editor
If the unfairness is considered problematic then the code of getRandomTokenIdFromFund should be enhanced.
#0 - cemozerr
2021-05-25T22:21:39Z
🌟 Selected for report: gpersoon
gpersoon
The function swapTo of NFTXVaultUpgradeable.sol is kind of a combination of mintTo and redeemTo (the code looks very similar to a combination of mintTo and redeemTo). Before receiveNFTs I would expect a call to allValidNFTs, like in mintTo. This is to make sure only eligible NFTs are transferred. Without this check any NFT could be transferred.
After withdrawNFTsTo I would expect a call to afterRedeemHook, like in redeemTo. The afterRedeemHook fixes the administration for the eligability. Without this the eligibility administrations would be flawed.
This way swapTo would circumvent the checks of mintTo and redeemTo and would allow any NFT to be transferred (even the one's that are not eligible)
NFTXVaultUpgradeable.sol
function mintTo(..) ... require(allValidNFTs(tokenIds), "NFTXVault: not eligible"); uint256 count = receiveNFTs(tokenIds, amounts);
function redeemTo(uint256 amount, uint256[] memory specificIds, address to) .... uint256[] memory redeemedIds = withdrawNFTsTo(amount, specificIds, to); afterRedeemHook(redeemedIds);
function swapTo(..) ... uint256 count = receiveNFTs(tokenIds, amounts); ... uint256[] memory ids = withdrawNFTsTo(count, specificIds, to);
Editor
Check if it necessary to also add the following statements in swapTo:
#0 - 0xKiwi
2021-05-20T22:27:49Z
Nice catch!
gpersoon
The function getRandomTokenIdFromFund of NFTXVaultUpgradeable.sol is not really random, as noted in the name of the function getPseudoRand. The value of the blockhash(block.number - 1) is fully determined for al the transactions in the same block. The result is that the retrieval of NFS's (via redeemTo, swapTo, withdrawNFTsTo, while enableDirectRedeem==false) can be manipulated. You can make a smart contract that tries to call redeemTo or swapTo, checks the resulting NFTs and reverts if it has NFTs that are unwanted.
function getPseudoRand(uint256 modulus) internal virtual returns (uint256) { randNonce += 1; return uint256( keccak256( abi.encodePacked(blockhash(block.number - 1), randNonce) ) ) % modulus; }
Editor
If the value or desirability of the NFT's are different then it's important to use other ways to randomize, for example via a random oracle or a commit/reveal schema.
#0 - cemozerr
2021-05-25T22:23:44Z
145.7175 USDC - $145.72
gpersoon
If you want to redeem or swap ERC1155 NFTs, the function withdrawNFTsTo (of NFTXVaultUpgradeable.sol) is used. When using specificIds, this function does not explicitly do something with the fact that you can have multiple NFTs of the same tokenId in ERC1155 and transfers them 1 by 1. (to be able to transfer 10 items of the same tokenId, you even have to specify 10x the same tokenId in the specificIds)
Also the function afterRedeemHook isn't optimized for ERC1155 NFTs, it will call _setUniqueEligibilities with a perhaps large array with multiple of the same values using more gas than necessary.
In contrast the function receiveNFTs is optimized for ERC1155 NFTs.
NFTXVaultUpgradeable.sol function withdrawNFTsTo(..) for (uint256 i = 0; i < amount; i++) { ... if (_is1155) { IERC1155Upgradeable(_assetAddress).safeTransferFrom(address(this),to,tokenId,1,""); NFTXDenyEligibility.sol function afterRedeemHook(uint256[] calldata tokenIds) external override virtual { ... _setUniqueEligibilities(tokenIds, true); } }
UniqueEligibility.sol function _setUniqueEligibilities(.. ) internal virtual { for (uint256 i = 0; i < tokenIds.length; i++) { uint256 tokenId = tokenIds[i]; ...
Editor
Consider optimizing the functions withdrawNFTsTo & afterRedeemHook for ERC1155 NFTs