The contract provides blockProfile as an owner-only moderation tool intended to remove a user from the platform. The implementation burns the token and deletes all profile state:
The only guard that prevents duplicate minting in mintProfile is:
Because blockProfile resets profileToToken[blockAddress] to 0 via delete, this guard passes immediately for the blocked address. The blocked user can call mintProfile in the very next transaction with the same wallet and receive a fresh token — the block has no lasting effect.
The same path exists for self-burns via burnProfile, which also deletes profileToToken[msg.sender]. A user who burns their own profile (whether to evade a pending moderation action or for any other reason) can re-mint without any admin approval.
Three compounding problems make this severe in practice:
No blocklist exists. There is no separate mapping recording that an address has been blocked. Once blockProfile runs, all record of the moderation action is erased on-chain. The admin has no way to tell a previously-blocked address from a brand-new user.
No unblock mechanism. Because there is no blocklist, the admin cannot selectively re-admit a user who was blocked by mistake. Every block is permanent and irreversible for the admin, yet completely ineffective at keeping the user out.
Same address, no friction. The blocked or self-burned user does not need a new wallet. They re-enter with the exact same address, bypassing any off-chain reputation or history tied to that address.
In a real-world dating platform, burning or blocking an account is expected to carry lasting consequences — removal from matching pools, loss of accumulated reputation, and prevention of immediate re-entry. None of these hold here.
The owner's only moderation capability is completely non-functional. A user who is blocked for abusive behaviour can rejoin in one transaction with the same address, same reputation history visible off-chain, and no admin approval required. Self-burned users similarly face no cooldown or re-admission barrier. Platform safety and moderation are broken.
PoC 1 — Blocked user re-mints: testPoC_BlockedUserCanRemint in test/testSoulboundProfileNFT.t.sol
PoC 2 — Self-burn re-mints: testPoC_SelfBurnAndRemint in test/testSoulboundProfileNFT.t.sol
Run with: forge test --match-test "testPoC_BlockedUserCanRemint|testPoC_SelfBurnAndRemint" -vvv
Introduce a persistent blocklist that is checked independently of the token mapping, and remove the block record only through an explicit admin unblock:
For self-burned users, decide at the protocol design level whether re-minting should be freely allowed or gated behind an admin allowlist. The current behaviour (free re-entry) may or may not be intentional, but it should be an explicit design choice rather than an accidental consequence of the delete pattern.
## 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.