DatingDapp

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

Blocked Users Can Re-Mint Profiles

Root + Impact

Description

  • When the admin blocks a user via `blockProfile()`, the user's profile should be permanently removed and they should be prevented from creating new profiles in the future.

  • The `blockProfile()` function only deletes the user's current profile data but does not maintain a blocklist. A blocked user can immediately call `mintProfile()` again since the only check is `profileToToken[msg.sender] == 0`, which passes after the profile is deleted.

// SoulboundProfileNFT.sol:57-66
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
@> delete profileToToken[blockAddress]; // Sets to 0
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
@> // BUG: No blocklist maintained - user can re-mint immediately
}
// SoulboundProfileNFT.sol:30-41
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
@> require(profileToToken[msg.sender] == 0, "Profile already exists"); // Passes after block!
// ... mint logic proceeds normally for blocked users ...
uint256 tokenId = ++_nextTokenId;
_safeMint(msg.sender, tokenId);
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
}

Risk

Likelihood:

  • Any user who gets blocked will discover they can re-mint within the same transaction or block

  • Malicious actors actively look for bypass mechanisms when moderated

Impact:

  • Admin blocking functionality is completely ineffective

  • Malicious/spam users cannot be permanently removed from the platform

  • Creates an endless cat-and-mouse game between admin and bad actors

  • Undermines all platform moderation capabilities

Proof of Concept

Scenario:
1. Malicious user mints profile (tokenId = 1)
2. Admin blocks user (profileToToken = 0)
3. Malicious user mints again (tokenId = 2) - **SHOULD FAIL BUT SUCCEEDS**
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../../src/SoulboundProfileNFT.sol";
/**
* @title F-003 PoC: Blocked Users Can Re-Mint Profiles
* @notice Demonstrates that users blocked by admin can immediately
* create a new profile, defeating the purpose of blocking
*/
contract F003_BlockedUsersCanRemintTest is Test {
SoulboundProfileNFT public profileNFT;
address owner = address(this);
address maliciousUser = makeAddr("malicious");
function setUp() public {
profileNFT = new SoulboundProfileNFT();
}
/**
* @notice Proves blocked users can immediately re-mint profiles
*/
function testBlockedUserCanRemint() public {
// Step 1: Malicious user mints a profile
vm.prank(maliciousUser);
profileNFT.mintProfile("BadActor", 25, "ipfs://bad");
uint256 firstTokenId = profileNFT.profileToToken(maliciousUser);
assertEq(firstTokenId, 1, "First token minted");
console.log("Malicious user minted profile with tokenId:", firstTokenId);
// Step 2: Admin blocks the user
profileNFT.blockProfile(maliciousUser);
uint256 tokenAfterBlock = profileNFT.profileToToken(maliciousUser);
assertEq(tokenAfterBlock, 0, "Profile deleted after block");
console.log("Admin blocked user, profileToToken is now:", tokenAfterBlock);
// Step 3: Malicious user immediately re-mints
// This SHOULD fail but DOESN'T
vm.prank(maliciousUser);
profileNFT.mintProfile("BadActor2", 26, "ipfs://bad2");
uint256 secondTokenId = profileNFT.profileToToken(maliciousUser);
assertEq(secondTokenId, 2, "User re-minted with new tokenId");
console.log("Malicious user re-minted with tokenId:", secondTokenId);
// VULNERABILITY: Block is completely ineffective
// User can keep getting blocked and re-minting forever
}
/**
* @notice Proves block+remint cycle can repeat indefinitely
*/
function testBlockRemintCycleRepeats() public {
for (uint256 i = 0; i < 3; i++) {
// User mints
vm.prank(maliciousUser);
profileNFT.mintProfile(
string(abi.encodePacked("Name", i)),
25,
"ipfs://image"
);
uint256 tokenId = profileNFT.profileToToken(maliciousUser);
assertEq(tokenId, i + 1, "Token minted");
// Admin blocks
profileNFT.blockProfile(maliciousUser);
assertEq(profileNFT.profileToToken(maliciousUser), 0, "Blocked");
}
// User can still mint after being blocked 3 times
vm.prank(maliciousUser);
profileNFT.mintProfile("FinalName", 25, "ipfs://final");
assertEq(profileNFT.profileToToken(maliciousUser), 4, "Still can mint");
}
/**
* @notice Shows expected blocklist behavior that's missing
*/
function testBlocklistMechanismMissing() public {
// Expected behavior with proper blocklist:
// mapping(address => bool) public blockedAddresses;
//
// function blockProfile(address user) external onlyOwner {
// blockedAddresses[user] = true; // <-- Missing!
// // ... delete profile ...
// }
//
// function mintProfile(...) external {
// require(!blockedAddresses[msg.sender], "User is blocked"); // <-- Missing!
// // ... mint logic ...
// }
// Current implementation only deletes profile, doesn't maintain blocklist
}
}

Recommended Mitigation

+ mapping(address => bool) public blockedAddresses;
function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
+ blockedAddresses[blockAddress] = true;
_burn(tokenId);
delete profileToToken[blockAddress];
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
+ require(!blockedAddresses[msg.sender], "User is blocked");
require(profileToToken[msg.sender] == 0, "Profile already exists");
// ... existing logic ...
}
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!