Summary
[L-03] Unsafe ERC721 token minting/transferring and lack of onERC721Received in EggVault contract
Vulnerability Details
The EggstravaganzaNFT contract used by EggHuntGame is minting ERC721 tokens in an unsafe manner. The contract uses the _mint() function instead of _safeMint(), creating new egg NFTs. In addition the contracts EggHuntGame and EggVault are transferring the NFTs using the transferFrom() function instead of safeTransferFrom(). This all violates the ERC721 safety standards.
EggstravaganzaNFT contract:
function mintEgg(address to, uint256 tokenId) external returns (bool) {
require(msg.sender == gameContract, "Unauthorized minter");
_mint(to, tokenId);
totalSupply += 1;
return true;
}
EggHuntGame contract:
function depositEggToVault(uint256 tokenId) external {
require(eggNFT.ownerOf(tokenId) == msg.sender, "Not owner of this egg");
eggNFT.transferFrom(msg.sender, address(eggVault), tokenId);
eggVault.depositEgg(tokenId, msg.sender);
}
EggVault contract:
function withdrawEgg(uint256 tokenId) public {
require(storedEggs[tokenId], "Egg not in vault");
require(eggDepositors[tokenId] == msg.sender, "Not the original depositor");
storedEggs[tokenId] = false;
delete eggDepositors[tokenId];
eggNFT.transferFrom(address(this), msg.sender, tokenId);
emit EggWithdrawn(msg.sender, tokenId);
}
Impact
The contracts EggstravaganzaNFT, EggHuntGame and EggVault doesn't validate whether the recipients are capable of handling ERC721 tokens
This violates ERC721 standards which recommends using:
_safeMint() instead of _mint()
safeTransferFrom() instead of transferFrom()
Tools Used
Manual review
Recommendations
Use _safeMint() instead of _mint() in function EggstravaganzaNFT::mintEgg():
function mintEgg(address to, uint256 tokenId) external returns (bool) {
require(msg.sender == gameContract, "Unauthorized minter");
- _mint(to, tokenId);
+ _safeMint(to, tokenId);
totalSupply += 1;
return true;
}
Implement the IERC721Receiver interface in EggVault contract:
+ import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
- contract EggVault is Ownable {
+ contract EggVault is IERC721Receiver, Ownable {
/*
* Existing functionality
*/
+ function onERC721Received(
+ address, /*operator*/
+ address, /*from*/
+ uint256, /*tokenId*/
+ bytes calldata /*data*/
+ ) external pure override returns (bytes4) {
+ return this.onERC721Received.selector;
+ }
}
Use safeTransferFrom() instead of transferFrom() in function EggHuntGame::depositEggToVault():
function depositEggToVault(uint256 tokenId) external {
require(eggNFT.ownerOf(tokenId) == msg.sender, "Not owner of this egg");
// The player must first approve the transfer on the NFT contract.
- eggNFT.transferFrom(msg.sender, address(eggVault), tokenId);
+ eggNFT.safeTransferFrom(msg.sender, address(eggVault), tokenId);
eggVault.depositEgg(tokenId, msg.sender);
}
Use safeTransferFrom() instead of transferFrom() in function EggVault::withdrawEgg():
function withdrawEgg(uint256 tokenId) public {
require(storedEggs[tokenId], "Egg not in vault");
require(eggDepositors[tokenId] == msg.sender, "Not the original depositor");
storedEggs[tokenId] = false;
delete eggDepositors[tokenId];
- eggNFT.transferFrom(address(this), msg.sender, tokenId);
+ eggNFT.safeTransferFrom(address(this), msg.sender, tokenId);
emit EggWithdrawn(msg.sender, tokenId);
}