DatingDapp

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

SoulboundProfileNFT::mintProfile allows re-minting after burn — user can reset identity, re-like profiles, and manipulate match rewards

Root + Impact

Description

  • When a user calls burnProfile() or the owner calls blockProfile(), the profileToToken mapping for that address is reset to 0 and the profile metadata is deleted. However, the mintProfile function only checks profileToToken[msg.sender] == 0 to determine if a profile already exists. Since burning sets this mapping back to 0, the user can immediately re-mint a new profile.

    This creates several exploit paths:

    1. Bypass blocking: An owner blocks a malicious user, but the user immediately re-mints a new profile with different data, rendering moderation useless.

    2. Reset likes and re-like: The likes mapping in LikeRegistry is keyed by address. After burning and re-minting, the likes[msg.sender][target] entries from the previous profile persist. However, a burned profile means profileToToken[msg.sender] returns 0, so the user re-mints, and can interact with the LikeRegistry again using a fresh identity while retaining old like state.

    3. Soulbound violation: The concept of a soulbound NFT implies a permanent, non-transferable identity. Allowing burn-and-remint breaks this invariant.

// Root cause in SoulboundProfileNFT.sol lines 30-31 and 44-54
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
// @> After burn, profileToToken[msg.sender] == 0, so user can re-mint
// ...
}
function burnProfile() external {
// ...
delete profileToToken[msg.sender]; // @> Resets to 0, enabling re-mint
// ...
}

Risk

Likelihood:

  • Any user can call burnProfile() and then mintProfile() at any time. This is trivially exploitable.

  • Blocked users can call mintProfile() immediately after being blocked since blocking also resets profileToToken to 0.

Impact:

  • Platform moderation is completely ineffective — blocked users can re-enter immediately.

  • Users can create new identities to manipulate the dating/matching system while retaining ETH deposited under their address.

Proof of Concept

The first test shows a user minting a profile, burning it, and immediately re-minting with a completely different identity — proving soulbound permanence is broken. The second test shows a blocked user calling mintProfile right after being blocked by the owner, bypassing moderation entirely.

function testH03_ReMintAfterBurn() public {
// User mints a profile
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://alice");
uint256 tokenId1 = soulboundNFT.profileToToken(user);
assertEq(tokenId1, 1);
// User burns their profile
vm.prank(user);
soulboundNFT.burnProfile();
assertEq(soulboundNFT.profileToToken(user), 0);
// User immediately re-mints with completely different identity
vm.prank(user);
soulboundNFT.mintProfile("Bob", 30, "ipfs://bob");
uint256 tokenId2 = soulboundNFT.profileToToken(user);
assertEq(tokenId2, 2); // New token, new identity, same address
}
function testH03_BypassBlock() public {
vm.prank(user);
soulboundNFT.mintProfile("BadActor", 25, "ipfs://bad");
// Owner blocks the user
soulboundNFT.blockProfile(user);
assertEq(soulboundNFT.profileToToken(user), 0);
// Blocked user immediately re-mints — moderation bypassed
vm.prank(user);
soulboundNFT.mintProfile("GoodActor", 25, "ipfs://good");
assertGt(soulboundNFT.profileToToken(user), 0); // Back on the platform!
}

Recommended Mitigation

Introduce a separate blockedAddresses mapping that persists even after burns. Prevent re-minting for blocked addresses. For voluntary burns, consider whether re-minting should be allowed or add a cooldown period.

+ mapping(address => bool) public blockedAddresses;
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(profileToToken[msg.sender] == 0, "Profile already exists");
+ require(!blockedAddresses[msg.sender], "Address is blocked");
// ...
}
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);
}
Updates

Lead Judging Commences

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