Summary
Due to missing blocked profile handling, any blocked profile can call SoulboundProfileNFT::mintProfile
again to mint and take part on the system. The purpose of blocking a profile no longer valid, since they could mint a new profile and continue interacting with existing LikeRegistry
and MultiSig
.
Vulnerability Details
When SoulboundProfileNFT::blockProfile
is called by the owner, it will burn the profile token, delete onchain variable related to the burnt tokenId and delete the profileToToken of the address.
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
So after the SoulboundNFT::blockProfile
being called, no one can call LikeRegistry::likeUser
to the blocked address.
function likeUser(address liked) external payable {
[...]
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
[...]
}
The LikeRegistry::likeUser
require both the caller to have existing profile tokens. So blocked user unable to like or being liked.
But the SoulboundProfileNFT::mintProfile
doesn't have any check if the user or profile is blocked or not. So if someone has already blocked, they can call the mint function again.
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
There is no check if sender has its profile blocked or not previously.
POC
function testBlockedProfileMintAgain() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 tokenId = soulboundNFT.profileToToken(user);
assertEq(tokenId, 1, "Token should exist before blocking");
vm.prank(owner);
soulboundNFT.blockProfile(user);
uint256 newTokenId = soulboundNFT.profileToToken(user);
assertEq(newTokenId, 0, "Token should be removed after blocking");
vm.prank(user);
vm.expectRevert();
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
}
Impact
Any blocked profile can mint again and continue their activity at this protocol
Blocked profile can mint again and mint a profile from another wallet, then make the other profile to match with his blocked account, and withdraw all of their own fund.
Recommendations
Add new storage variable to check if a wallet address is blocked or not.
@@ -20,6 +20,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
mapping(address => uint256) public profileToToken; // Maps user to their profile NFT
mapping(uint256 => Profile) private _profiles; // Stores profile metadata
+ mapping(address => bool) public profileBlocked;
event ProfileMinted(address indexed user, uint256 tokenId, string name, uint8 age, string profileImage);
event ProfileBurned(address indexed user, uint256 tokenId);
@@ -29,6 +30,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
/// @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(profileBlocked[msg.sender] == false, "Profile is blocked");
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
@@ -61,6 +63,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
+ profileBlocked[blockAddress] = true;
emit ProfileBurned(blockAddress, tokenId);
}