When a user calls burnProfile() or the owner calls blockProfile(), the profileToToken mapping for that address is reset to 0 and the profile metadata is deleted. However, the mintProfile function only checks profileToToken[msg.sender] == 0 to determine if a profile already exists. Since burning sets this mapping back to 0, the user can immediately re-mint a new profile.
This creates several exploit paths:
Bypass blocking: An owner blocks a malicious user, but the user immediately re-mints a new profile with different data, rendering moderation useless.
Reset likes and re-like: The likes mapping in LikeRegistry is keyed by address. After burning and re-minting, the likes[msg.sender][target] entries from the previous profile persist. However, a burned profile means profileToToken[msg.sender] returns 0, so the user re-mints, and can interact with the LikeRegistry again using a fresh identity while retaining old like state.
Soulbound violation: The concept of a soulbound NFT implies a permanent, non-transferable identity. Allowing burn-and-remint breaks this invariant.
Likelihood:
Any user can call burnProfile() and then mintProfile() at any time. This is trivially exploitable.
Blocked users can call mintProfile() immediately after being blocked since blocking also resets profileToToken to 0.
Impact:
Platform moderation is completely ineffective — blocked users can re-enter immediately.
Users can create new identities to manipulate the dating/matching system while retaining ETH deposited under their address.
The first test shows a user minting a profile, burning it, and immediately re-minting with a completely different identity — proving soulbound permanence is broken. The second test shows a blocked user calling mintProfile right after being blocked by the owner, bypassing moderation entirely.
Introduce a separate blockedAddresses mapping that persists even after burns. Prevent re-minting for blocked addresses. For voluntary burns, consider whether re-minting should be allowed or add a cooldown period.
## 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.