DatingDapp

AI First Flight #6
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

M-01: blockProfile() does not permanently ban an address, allowing a blocked user to recreate a profile

M-01: blockProfile() does not permanently ban an address, allowing a blocked user to recreate a profile

Severity: Medium

Summary

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.


Vulnerability Details

In mintProfile():

require(profileToToken[msg.sender] == 0, "Profile already exists");

This means any address with profileToToken[address] == 0 is allowed to mint.

In blockProfile():

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);
}

The critical issue is:

delete profileToToken[blockAddress];

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.


Impact

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.


Root Cause

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.


Internal Preconditions

  • SoulboundProfileNFT is deployed

  • the owner/admin can call blockProfile()

External Preconditions

  • a user has minted a profile

  • the owner blocks that user

  • the blocked user attempts to mint again


Attack Path / Failure Path

  1. User mints a profile.

  2. Owner calls blockProfile(user).

  3. The function burns the NFT and deletes profileToToken[user].

  4. profileToToken[user] becomes 0.

  5. User calls mintProfile() again.

  6. The check profileToToken[msg.sender] == 0 passes.

  7. A new profile is minted for the previously blocked user.


Proof of Concept

The following Foundry test demonstrates the issue:

function test_BlockedUserCanMintAgain() public {
address user = address(0x123);
SoulboundProfileNFT nft = new SoulboundProfileNFT();
// User mints initial profile
vm.prank(user);
nft.mintProfile("Alice", 25, "ipfs://alice");
uint256 firstTokenId = nft.profileToToken(user);
assertEq(firstTokenId, 1);
// Owner blocks the user
nft.blockProfile(user);
// Mapping is cleared
assertEq(nft.profileToToken(user), 0);
// Blocked user mints again
vm.prank(user);
nft.mintProfile("AliceAgain", 26, "ipfs://alice2");
uint256 secondTokenId = nft.profileToToken(user);
// User successfully recreated profile
assertTrue(secondTokenId != 0);
assertEq(secondTokenId, 2);
}

PoC Explanation

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


Recommended Mitigation

Track blocked users separately and prevent them from minting again.

One straightforward fix is to add a dedicated mapping:

mapping(address => bool) public isBlocked;

Then update blockProfile():

function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
isBlocked[blockAddress] = true;
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}

And update mintProfile():

function mintProfile(
string memory name,
uint8 age,
string memory profileImage
) external {
require(!isBlocked[msg.sender], "Address is blocked");
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);
}

If you want blocks to be reversible, add an owner-only unblockProfile(address) function.


Alternative Design Note

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.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-01] `SoulboundProfileNFT::blockProfile` make it possible to recreate the profile

## 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); } ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!