DatingDapp

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

Blocked users can bypass blockProfile() and mint a new profile

Root + Impact

Description

  • The blockProfile() function is intended to allow the contract owner to block a user from the platform.

  • However, the current implementation only burns the user's existing profile NFT and deletes the associated mappings. The contract does not maintain any state that marks the user as permanently blocked. Because of this, once the user's profile is deleted, the condition inside mintProfile() becomes valid again:
    require(profileToToken[msg.sender] == 0, "Profile already exists");

  • After being blocked, the user's profileToToken mapping entry becomes 0, allowing them to call mintProfile() again and create a new profile immediately. As a result, the moderation mechanism is ineffective because blocked users can simply re-register themselves by minting a new profile.

// The blockProfile() function only removes the user's existing profile but does not record the blocked status of the address.
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);
}

Risk

Likelihood:

  • Any user that gets blocked can simply call mintProfile() again because the contract does not store a blocked flag.

Impact:

  • The purpose of blockProfile() is to remove malicious or abusive users from the system. However, since blocked users can immediately mint a new profile, the moderation mechanism becomes ineffective. This allows malicious users to bypass moderation and rejoin the platform without restrictions.

Proof of Concept

The following Foundry test demonstrates that a blocked user can mint a new profile after being blocked

function testBlockedUserCanMintAgain() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 tokenId1 = soulboundNFT.profileToToken(user);
//Now the owner blocks the user
vm.prank(owner);
soulboundNFT.blockProfile(user);
//the mapping should reset after blocking
assertEq(soulboundNFT.profileToToken(user), 0);
//The user tries to mint again after being blocked
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 tokenId2 = soulboundNFT.profileToToken(user);
assertTrue(tokenId1 != 0, "First token ID shouldn't be 0 after minting");
assertTrue(tokenId2 !=0, "Second token ID shouldn't be 0 after minting again");
// proof that a new profile was created after blocking the user, since the token ID is different from the first one
assertTrue(tokenId2 != tokenId1, "User was able to mint a new profile after being blocked");
}

Recommended Mitigation

Add a checking in mintProfile():

require(!isBlocked[msg.sender], "User is blocked");

Add a mapping like this:

mapping(address => bool) public isBlocked;
// Update blockProfile function
function blockProfile(address blockAddress) external onlyOwner {
isBlocked[blockAddress] = true;
uint256 tokenId = profileToToken[blockAddress];
if (tokenId != 0) {
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
}
emit ProfileBurned(blockAddress, tokenId);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!