SoulboundProfileNFT::blockProfile lets the app owner "block users" by burning their profile NFT. But it never records that the address was blocked anywhere -- there is no block-list, no cooldown, and mintProfile's only gate is profileToToken[msg.sender] == 0. Since burning deletes profileToToken[blockAddress] back to 0, the blocked user can call mintProfile again in the very next transaction and keep using the app under a fresh profile, completely defeating the moderation feature.
blockProfile and burnProfile (the user's own self-service delete) do the exact same thing to storage -- there is no distinction recorded anywhere between "this user deleted their own profile" and "the app owner blocked this user for cause." Both simply reset profileToToken[user] to 0, which is the only condition mintProfile checks before allowing a fresh registration.
Likelihood:
Trivially reproducible by any blocked user -- no special conditions, just call mintProfile again with any (even identical) profile data.
Impact:
The blockProfile feature provides no actual, lasting moderation capability -- a user blocked for abusive behavior, harassment, or any other cause can circumvent the block immediately and indefinitely, at no cost beyond gas. This undermines user trust/safety guarantees the feature is meant to provide, though it does not directly cause fund loss.
Run with forge test --match-test test_M1_blockProfileDoesNotPreventReRegistration -vv. The blocked address successfully mints a brand new profile on the very next call.
Track blocked addresses explicitly and check that list in mintProfile:
## 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.