First Flight #12: Kitty Connect

First Flight #12: Kitty Connect
Beginner FriendlyFoundryNFTGameFi
100 EXP
View results
Submission Details
Severity: high
Invalid

NFTs can be minted to arbitrary users

Summary

Shop partners can mint cats to arbitrary users.

Vulnerability Details

Docs say users need to buy cats in order to get an NFT minted, but there is no state variable which shows users who should receive an NFT. Furthermore, the KittyConnect::mintCatToNewOwner function allows a new catOwner who might not have bought a cat, therefore they could receive an NFT without paying.

Impact

Users could receive undefined number of NFTs without paying if desired by shop partners, these could inflate the number of active NFTs and thus decrease their value.

Tools Used

Manual review
Foundry testing

POC:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {DeployKittyConnect} from "../script/DeployKittyConnect.s.sol";
import {HelperConfig} from "../script/HelperConfig.s.sol";
import {KittyConnect} from "../src/KittyConnect.sol";
import {KittyBridge, KittyBridgeBase, Client} from "../src/KittyBridge.sol";
contract KittyTest is Test {
KittyConnect kittyConnect;
KittyBridge kittyBridge;
HelperConfig helperConfig;
HelperConfig.NetworkConfig networkConfig;
address kittyConnectOwner;
address partnerA;
address partnerB;
address user;
address ethUsdPriceFeed;
event ShopPartnerAdded(address partner);
event CatMinted(uint256 tokenId, string catIpfsHash);
event TokensRedeemedForVetVisit(
uint256 tokenId,
uint256 amount,
string remarks
);
event CatTransferredToNewOwner(
address prevOwner,
address newOwner,
uint256 tokenId
);
function setUp() external {
DeployKittyConnect deployer = new DeployKittyConnect();
(kittyConnect, helperConfig) = deployer.run();
networkConfig = helperConfig.getNetworkConfig();
kittyConnectOwner = kittyConnect.getKittyConnectOwner();
partnerA = kittyConnect.getKittyShopAtIdx(0);
partnerB = kittyConnect.getKittyShopAtIdx(1);
kittyBridge = KittyBridge(kittyConnect.getKittyBridge());
user = makeAddr("user");
}
function test_mintUnlimitedCats() public{
vm.startPrank(partnerA);
kittyConnect.mintCatToNewOwner(user, "hash", "Toby", "littleCat", 7);
kittyConnect.mintCatToNewOwner(user, "hash", "Toby", "littleCat", 18);
kittyConnect.mintCatToNewOwner(user, "hash", "Toby", "littleCat", 35);
kittyConnect.mintCatToNewOwner(user, "hash", "Toby", "littleCat", 7);
assertEq(kittyConnect.getTokenCounter(), 4);
}
}

Recommendations

Using a mapping to tell whether a user can receive an NFT or not, there should be a function for a user to buy the NFT as well:

contract KittyConnect is ERC721 {
.
.
.
mapping(address => bool) private s_isKittyShop;
address[] private s_kittyShops;
mapping(address user => uint256[]) private s_ownerToCatsTokenId;
+ mapping(address => bool) private s_canReceiveCat;
mapping(uint256 tokenId => CatInfo) private s_catInfo;
.
.
.
+function buyCat() external payable{
+if(msg.value > 1 ether) s_canReceiveCat[msg.sender] = true;
+}
.
.
.
function mintCatToNewOwner(address catOwner, string memory catIpfsHash, string memory catName, string memory breed, uint256 dob) external onlyShopPartner {
require(!s_isKittyShop[catOwner], "KittyConnect__CatOwnerCantBeShopPartner");
+ require(s_canReceiveCat[catOwner], "This user has not bought any cats!");
+ s_canReceiveCat[catOwner] = false;
uint256 tokenId = kittyTokenCounter;
kittyTokenCounter++;
s_catInfo[tokenId] = CatInfo({
catName: catName,
breed: breed,
image: catIpfsHash,
dob: dob,
prevOwner: new address[](0),
shopPartner: msg.sender,
idx: s_ownerToCatsTokenId[catOwner].length
});
s_ownerToCatsTokenId[catOwner].push(tokenId);
_safeMint(catOwner, tokenId);
emit CatMinted(tokenId, catIpfsHash);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.