DatingDapp

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

Blocked users can evade bans by re-minting their profile

Root + Impact

Description

  • The Function blockProfile() function is used to block a user from the SoulboundProfileNFT permanently.

  • Issue arises when the Owner calls the blockProfile() for intented user it deletes the user profile, but it wont have any constraints that stops blocked user to be a valid user again. The function did not have any mapping mechanism for the blocked users that maps the address and stop the blocked user to mint NFT again.

function mintProfile(
string memory name,
uint8 age,
string memory profileImage
) external {
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];
emit ProfileBurned(blockAddress, tokenId);
}

Risk

Likelihood:

  • The malicious user can mint NFT even after getting blocked which makes the blockProfile() function completely useless.


Impact:

  • The platform's ban mechanism is entirely ineffective. Malicious actors, scammers, or spammers can continuously evade bans and rejoin the platform without needing to create new wallets. This renders community moderation useless and severely degrades the safety and trust of the dating platform.

Proof of Concept

  • The following Foundry test demonstrates how an attacker (spider) can successfully evade a ban. It proves that immediately after the contract owner calls blockProfile to remove Spider, he can successfully call mintProfile and receive a new token, completely bypassing the intended restriction.

function test_spider_can_be_a_user_again_after_owner_blocks()
public
mintsTheUser
{
//Arrange
vm.startPrank(spider);
uint256 balance_Of_nft_Afterminting = soulNFT.balanceOf(spider);
uint256 tokenId_for_minted_token = soulNFT.profileToToken(spider);
vm.stopPrank();
//Act
vm.startPrank(DEFAULT_FOUNDRY);
soulNFT.blockProfile(spider);
vm.stopPrank();
uint256 balance_Of_nft_after_owner_blocks_spider = soulNFT.balanceOf(
spider
);
uint256 TokenId_Of_spider_after_ower_blocks_spider = soulNFT
.profileToToken(spider);
vm.startPrank(spider);
soulNFT.mintProfile("spider", 21, "spider");
uint256 balance_Of_nft_after_spider_minted_again = soulNFT.balanceOf(
spider
);
uint256 tokenId_For_MintedToken_After_2minted_Token = soulNFT
.profileToToken(spider);
//Assert
//Initially
assertEq(balance_Of_nft_Afterminting, 1, "should be 1");
assertEq(tokenId_for_minted_token, 2, "should be 1");
//After getting blocked by owner itself
assertEq(
balance_Of_nft_after_owner_blocks_spider,
0,
"should be 0 as owner blocks the user"
);
assertEq(
TokenId_Of_spider_after_ower_blocks_spider,
0,
"Should be 0 as owner blocks the user"
);
// After Alice evades the ban by minting again
assertEq(
balance_Of_nft_after_spider_minted_again,
1,
"should be 1 as spider is minted again"
);
assertEq(
tokenId_For_MintedToken_After_2minted_Token,
3,
"should be 3 as spider is minted again"
);
}
[PASS] test_spider_can_be_a_user_again_after_owner_blocks() (gas: 389255)
Traces:
[491834] TestVulnDapp::test_spider_can_be_a_user_again_after_owner_blocks()
├─ [0] VM::prank(Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea])
│ └─ ← [Return]
├─ [165657] SoulboundProfileNFT::mintProfile("Alice", 18, "Hello")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea], tokenId: 1)
│ ├─ emit ProfileMinted(user: Alice: [0xBf0b5A4099F0bf6c8bC4252eBeC548Bae95602Ea], tokenId: 1, name: "Alice", age: 18, profileImage: "Hello")
│ └─ ← [Stop]
├─ [0] VM::prank(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Return]
├─ [143757] SoulboundProfileNFT::mintProfile("spider", 21, "spider")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 2)
│ ├─ emit ProfileMinted(user: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 2, name: "spider", age: 21, profileImage: "spider")
│ └─ ← [Stop]
├─ [0] VM::startPrank(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Return]
├─ [930] SoulboundProfileNFT::balanceOf(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 1
├─ [913] SoulboundProfileNFT::profileToToken(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 2
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ └─ ← [Return]
├─ [11401] SoulboundProfileNFT::blockProfile(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ ├─ emit Transfer(from: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], to: 0x0000000000000000000000000000000000000000, tokenId: 2)
│ ├─ emit ProfileBurned(user: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 2)
│ └─ ← [Stop]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [930] SoulboundProfileNFT::balanceOf(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 0
├─ [913] SoulboundProfileNFT::profileToToken(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::startPrank(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63])
│ └─ ← [Return]
├─ [139757] SoulboundProfileNFT::mintProfile("spider", 21, "spider")
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 3)
│ ├─ emit ProfileMinted(user: spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], tokenId: 3, name: "spider", age: 21, profileImage: "spider")
│ └─ ← [Stop]
├─ [930] SoulboundProfileNFT::balanceOf(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 1
├─ [913] SoulboundProfileNFT::profileToToken(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63]) [staticcall]
│ └─ ← [Return] 3
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.36ms (257.67µs CPU time)

Recommended Mitigation

  • To resolve this issue, the contract must separate the concept of "not owning a profile" from "being banned."

    • Add a mapping to track addresses that have been permanently blocked.

    • Flag the user as blocked when the owner removes them.

    • Add a strict check to ensure banned users cannot interact with the minting function.

mapping(address => bool) public isBlocked;// Maps the blocked user
//Update the blockProfile() that set the bool of block address to true
function blockProfile(address blockAddress) external onlyOwner {
// ... existing logic ...
isBlocked[blockAddress] = true;
}
//Update the mintProfile() that checks the user's block status
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
require(!isBlocked[msg.sender], "Address is banned");
require(profileToToken[msg.sender] == 0, "Profile already exists");
// ... rest of the minting logic ...
}
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!