119 lines
4.4 KiB
Markdown
119 lines
4.4 KiB
Markdown
# Blocked User Can Call `SoulboundProfileNFT::mintProfile` Again
|
|
|
|
## Summary
|
|
|
|
Due to missing blocked profile handling, any blocked profile can call `SoulboundProfileNFT::mintProfile` again to mint and take part on the system. The purpose of blocking a profile no longer valid, since they could mint a new profile and continue interacting with existing `LikeRegistry` and `MultiSig`.
|
|
|
|
## Vulnerability Details
|
|
|
|
When `SoulboundProfileNFT::blockProfile` is called by the owner, it will burn the profile token, delete onchain variable related to the burnt tokenId and delete the profileToToken of the address.
|
|
|
|
```solidity
|
|
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);
|
|
}
|
|
```
|
|
|
|
So after the `SoulboundNFT::blockProfile` being called, no one can call `LikeRegistry::likeUser` to the blocked address.
|
|
|
|
```solidity
|
|
function likeUser(address liked) external payable {
|
|
[...]
|
|
|
|
require(profileNFT.profileToToken(msg.sender) != 0, "Must have a profile NFT");
|
|
require(profileNFT.profileToToken(liked) != 0, "Liked user must have a profile NFT");
|
|
|
|
[...]
|
|
}
|
|
```
|
|
|
|
The `LikeRegistry::likeUser` require both the caller to have existing profile tokens. So blocked user unable to like or being liked.
|
|
|
|
But the `SoulboundProfileNFT::mintProfile` doesn't have any check if the user or profile is blocked or not. So if someone has already blocked, they can call the mint function again.
|
|
|
|
```solidity
|
|
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);
|
|
}
|
|
```
|
|
|
|
There is no check if sender has its profile blocked or not previously.
|
|
|
|
## POC
|
|
|
|
```solidity
|
|
function testBlockedProfileMintAgain() public {
|
|
vm.prank(user);
|
|
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
|
|
|
|
uint256 tokenId = soulboundNFT.profileToToken(user);
|
|
assertEq(tokenId, 1, "Token should exist before blocking");
|
|
|
|
vm.prank(owner);
|
|
soulboundNFT.blockProfile(user);
|
|
|
|
uint256 newTokenId = soulboundNFT.profileToToken(user);
|
|
assertEq(newTokenId, 0, "Token should be removed after blocking");
|
|
|
|
vm.prank(user);
|
|
vm.expectRevert();
|
|
// blocked profile should not be able to mint from the same wallet address again
|
|
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
|
|
}
|
|
```
|
|
|
|
## Impact
|
|
|
|
- Any blocked profile can mint again and continue their activity at this protocol
|
|
- Blocked profile can mint again and mint a profile from another wallet, then make the other profile to match with his blocked account, and withdraw all of their own fund.
|
|
|
|
## Recommendations
|
|
|
|
Add new storage variable to check if a wallet address is blocked or not.
|
|
|
|
```diff
|
|
--- a/src/SoulboundProfileNFT.sol
|
|
+++ b/src/SoulboundProfileNFT.sol
|
|
@@ -20,6 +20,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
|
|
|
|
mapping(address => uint256) public profileToToken; // Maps user to their profile NFT
|
|
mapping(uint256 => Profile) private _profiles; // Stores profile metadata
|
|
+ mapping(address => bool) public profileBlocked;
|
|
|
|
event ProfileMinted(address indexed user, uint256 tokenId, string name, uint8 age, string profileImage);
|
|
event ProfileBurned(address indexed user, uint256 tokenId);
|
|
@@ -29,6 +30,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
|
|
/// @notice Mint a soulbound NFT representing the user's profile.
|
|
function mintProfile(string memory name, uint8 age, string memory profileImage) external {
|
|
require(profileToToken[msg.sender] == 0, "Profile already exists");
|
|
+ require(profileBlocked[msg.sender] == false, "Profile is blocked");
|
|
|
|
uint256 tokenId = ++_nextTokenId;
|
|
_safeMint(msg.sender, tokenId);
|
|
@@ -61,6 +63,7 @@ contract SoulboundProfileNFT is ERC721, Ownable {
|
|
_burn(tokenId);
|
|
delete profileToToken[blockAddress];
|
|
delete _profiles[tokenId];
|
|
+ profileBlocked[blockAddress] = true;
|
|
|
|
emit ProfileBurned(blockAddress, tokenId);
|
|
}
|
|
```
|
|
|