blockProfile() does not permanently ban an address, allowing a blocked user to recreate a profileSeverity: Medium
The SoulboundProfileNFT::blockProfile() function removes the blocked user’s NFT and then deletes profileToToken[blockAddress]. Because mintProfile() only checks whether profileToToken[msg.sender] == 0, the blocked address can simply call mintProfile() again and recreate a new profile.
As a result, the block action is not persistent. It behaves like a forced burn, not a permanent block.
In mintProfile():
This means any address with profileToToken[address] == 0 is allowed to mint.
In blockProfile():
The critical issue is:
That resets the mapping value to 0, which is the exact condition mintProfile() uses to allow minting. Therefore, after being blocked, the user is put back into a state that is indistinguishable from a never-before-seen user.
A blocked user can immediately recreate a new profile, defeating the purpose of moderation and account blocking.
This undermines the protocol’s trust and moderation model in several ways:
malicious or abusive users can rejoin after being blocked
admins cannot reliably enforce bans
downstream contracts may assume blocked users are removed permanently when they are not
users can cycle through profiles to evade enforcement
If blocking is intended to be permanent or long-lived, this is a meaningful business-logic flaw.
The contract uses the same state variable, profileToToken, for both:
tracking whether a user currently has a profile
determining whether a user is allowed to mint
After blocking, the code clears that state entirely instead of recording that the address was banned.
SoulboundProfileNFT is deployed
the owner/admin can call blockProfile()
a user has minted a profile
the owner blocks that user
the blocked user attempts to mint again
User mints a profile.
Owner calls blockProfile(user).
The function burns the NFT and deletes profileToToken[user].
profileToToken[user] becomes 0.
User calls mintProfile() again.
The check profileToToken[msg.sender] == 0 passes.
A new profile is minted for the previously blocked user.
The following Foundry test demonstrates the issue:
This test shows that:
the user initially mints successfully
the owner blocks the user
the block operation clears the profile mapping
the same address can mint again without restriction
therefore, blockProfile() does not enforce a persistent ban
Track blocked users separately and prevent them from minting again.
One straightforward fix is to add a dedicated mapping:
Then update blockProfile():
And update mintProfile():
If you want blocks to be reversible, add an owner-only unblockProfile(address) function.
If the intended behavior is not a permanent ban, then this should not be reported as a vulnerability. In that case, blockProfile() is really functioning as an admin-forced profile deletion, not a true block. The severity depends on the intended moderation model.
## Description The `SoulboundProfileNFT::blockProfile` function uses `delete profileToToken[blockAddress]`, which resets `profileToToken[blockAddress]` to `0`. Since the mintProfile function checks for an existing profile by verifying that `profileToToken[msg.sender] == 0`, a blocked account can be recreated by simply minting a new profile. This behavior bypasses the intended permanent block functionality. ## Vulnerability Details By deleting the mapping entry for a blocked account, the contract inadvertently allows a new mintProfile call to pass the check `require(profileToToken[msg.sender] == 0, "Profile already exists")`. Essentially, once an account is blocked, its associated mapping entry is cleared, so the condition to identify an account with an existing profile is no longer met. This loophole enables a blocked account to recreate its profile, undermining the purpose of blocking. ## Impact A blocked account, which should be permanently barred from engaging with the platform, can circumvent this restriction by re-minting its profile. The integrity of the platform is compromised, as blocked users could regain access and potentially perform further malicious actions. ## POC ```solidity function testRecereationOfBlockedAccount() public { // Alice mints a profile successfully vm.prank(user); soulboundNFT.mintProfile("Alice", 18, "ipfs://profileImageAlice"); // Owner blocks Alice's account, which deletes Alice profile mapping vm.prank(owner); soulboundNFT.blockProfile(user); // The blocked user (Alice) attempts to mint a new profile. // Due to the reset mapping value (0), the require check is bypassed. vm.prank(user); soulboundNFT.mintProfile("Alice", 18, "ipfs://profileImageAlice"); } ``` ## Recommendations - When blocking an account, implement a mechanism to permanently mark that address as blocked rather than simply deleting an entry. For example, maintain a separate mapping (e.g., isBlocked) to record blocked accounts, and update mintProfile to check if an account is permanently barred from minting: Example modification: ```diff + mapping(address => bool) public isBlocked; ... function mintProfile(string memory name, uint8 age, string memory profileImage) external { + require(!isBlocked[msg.sender], "Account is permanently blocked"); require(profileToToken[msg.sender] == 0, "Profile already exists"); 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); } ... function blockProfile(address blockAddress) external onlyOwner { uint256 tokenId = profileToToken[blockAddress]; require(tokenId != 0, "No profile found"); _burn(tokenId); delete profileToToken[blockAddress]; delete _profiles[tokenId]; + isBlocked[blockAddress] = true; emit ProfileBurned(blockAddress, tokenId); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.