Summary
Users can mint new profiles unlimited number of times with `SounboundProfileNFT::mintProfile`.
Vulnerability Details
When a user burns his profile with SounboundProfileNFT::burnProfile
, profileToToken[msg.sender]
is deleted and the value is again 0. After that they can mint a new profile. Also if the owner of the contract blocks a user with SoundboundProfileNFT::blockProfile
, the user with the blocked profile can just create a new one and bypass the block.
Impact
Users can continuously mint, burn and remint new profiles, allowing them to create multiple fake identities on the platform. Also if the contract owner blocks a profile by burning it, the user can simply mint a new one and continue participating.
PoC
Copy this test and paste it in testSoulboundProfileNFT.t.sol
function test_UserCanMintMultipleProfiles() public {
vm.startPrank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 firstTokenId = soulboundNFT.profileToToken(user);
assertEq(soulboundNFT.ownerOf(firstTokenId), user);
soulboundNFT.burnProfile();
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 secondTokenId = soulboundNFT.profileToToken(user);
assertEq(soulboundNFT.ownerOf(secondTokenId), user);
vm.stopPrank();
vm.startPrank(owner);
soulboundNFT.blockProfile(user);
vm.stopPrank();
vm.startPrank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 thirdTokenId = soulboundNFT.profileToToken(user);
assertEq(soulboundNFT.ownerOf(thirdTokenId), user);
vm.stopPrank();
}
Tools Used
-foundry
Recommendations
Adding a mapping of blocked users to prevent them from minting new profiles after contract owner calls SoundboundProfileNFT::blockProfile
.
+ mapping(address => bool) private _blockedUsers;
/// @notice Mint a soulbound NFT representing the user's profile.
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
+ require(!_blockedUsers[msg.sender], "The user is blocked");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
// Store metadata on-chain
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
/// @notice App owner can block users
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
+ _blockedUsers[blockAddress] = true;
emit ProfileBurned(blockAddress, tokenId);
}