DatingDapp

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

A Blocked User Can Immediately Mint A New NFT And Continue Using the LikeRegistry Exactly as Before, Making the Blocking Functionality Ineffective

A Blocked User Can Immediately Mint A New NFT And Continue Using the LikeRegistry Exactly as Before, Making the Blocking Functionality Ineffective

Description

The SoulboundNFT::blockProfile function presumably is used to remove a user from the platform when they misbehave. However, this blocking functionality only removes their current NFT. The blocked user can immediately mint a new NFT.

Further, because the LikeRegistry smart contract stores all of its data by address, not by NFT id, the previously blocked user retains all of their likes and matches from before they were blocked.

// Root cause in the codebase with @> marks to highlight the relevant section
/// @notice App owner can block users
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:

  • This issue occurs any time a user misbehaves and needs to be blocked

Impact:

  • This issue essentially makes the blocking functionality useless.

Proof of Concept

The following unit test shows that even if Alice is blocked, she can immediately mint a new SoulboundNFT with identical metadata. The only difference is the id of the NFT. Because the LikeRegistry stores data based on address and not NFT, Alice is able to resume using the LikeRegistry with all of her old likes and matches intact.

function testBlockingIsIneffective() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 originalId = soulboundNFT.profileToToken(user);
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 30, "ipfs://profileImage2");
vm.deal(user, 1 ether);
vm.prank(user);
likeRegistry.likeUser{value: 1 ether}(user2);
assertEq(likeRegistry.likes(user, user2), true);
//Suppose user Alice misbehaves and is blocked
vm.prank(owner);
soulboundNFT.blockProfile(user);
//She can immediately mint a new profile with identical metadata
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
uint256 newId = soulboundNFT.profileToToken(user);
//Of course, NFT ids are different
assertNotEq(originalId, newId);
//But Alice still retains her old likes, and is still able to receive likes and form matches
vm.deal(user2, 1 ether);
vm.prank(user2);
vm.expectEmit(address(likeRegistry));
emit LikeRegistry.Matched(user2, user);
likeRegistry.likeUser{value: 1 ether}(user);
}

Recommended Mitigation

Create a new SoulboundNFT::blockedUsers mapping to hold blocked addresses. This will prevent a formerly blocked user from being able to create a new NFT.

+ mapping(address => bool) public blockedUsers;
...
/// @notice Mint a soulbound NFT representing the user's profile.
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
+ require(blockedUsers[msg.sender] == 0, "User was blocked");
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);
}
/// @notice App owner can block users
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
+ blockedUsers[blockAddress] = true;
emit ProfileBurned(blockAddress, tokenId);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!