DatingDapp

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

blockProfile() does not persistently block a user

Description

  • Normal behavior: when the contract owner calls blockProfile(), the target address should be prevented from participating in the platform going forward.

  • Specific issue: blockProfile() only burns the token and deletes state — resetting the address to exactly the same state as one that never minted, with no persistent block flag anywhere.

function blockProfile(address blockAddress) external onlyOwner {
uint256 tokenId = profileToToken[blockAddress];
require(tokenId != 0, "No profile found");
_burn(tokenId);
delete profileToToken[blockAddress]; // @> resets state to identical to "never minted" — no isBlocked flag exists anywhere
delete _profiles[tokenId];
emit ProfileBurned(blockAddress, tokenId);
}

Risk

Likelihood:

  • Re-exploiting this requires nothing beyond calling mintProfile() again immediately after being blocked — no special precondition, no cost beyond normal mint gas.

Impact:

  • The owner's moderation action becomes functionally meaningless — a blocked user regains full platform access (a fresh profile NFT, ability to use LikeRegistry.likeUser()) immediately.

Proof of Concept

The owner blocks a user (troll), the test confirms the token is actually burned and profileToToken reset to 0, then troll immediately calls mintProfile() again in the very next transaction with zero obstruction — proving blockProfile() provides no lasting restriction whatsoever, only a one-time reset.

// test/PoC_FindingF.sol
function testBlockedUserCanImmediatelyRemint() public {
uint256 originalTokenId = nft.profileToToken(troll);
assertEq(originalTokenId, 1);
nft.blockProfile(troll); // owner blocks the troll
vm.expectRevert();
nft.ownerOf(originalTokenId); // original token burned
assertEq(nft.profileToToken(troll), 0);
vm.prank(troll);
nft.mintProfile("Troll", 30, "ipfs://troll-again"); // immediately re-mints, no obstruction
uint256 newTokenId = nft.profileToToken(troll);
assertEq(newTokenId, 2);
assertEq(nft.ownerOf(newTokenId), troll);
}
// [PASS] — gas 146035, verified via forge test

Recommended Mitigation

The fix separates "does this address currently hold a token" from "is this address permanently barred" — a new isBlocked mapping that is only ever set to true, never cleared, gives blockProfile() a lasting effect that mintProfile() can check independently of the token/NFT state that gets reset on every block.

+ mapping(address => bool) public isBlocked;
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);
}
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");
...
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!