Platform: Code4rena
Start Date: 17/03/2023
Pot Size: $36,500 USDC
Total HM: 10
Participants: 98
Period: 3 days
Judge: leastwood
Total Solo HM: 5
Id: 223
League: ETH
Rank: 3/98
Findings: 1
Award: $3,269.95
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: cccz
3269.9461 USDC - $3,269.95
ProfilePicture.getPFP will return information about the underlying NFT and when addressRegistry.getAddress(cidNFTID) ! = ERC721(nftContract).ownerOf(nftID)
, it will return 0.
But if the underlying NFT is burned, getPFP may return incorrect information
function getPFP(uint256 _pfpID) public view returns (address nftContract, uint256 nftID) { if (_ownerOf[_pfpID] == address(0)) revert TokenNotMinted(_pfpID); ProfilePictureData storage pictureData = pfp[_pfpID]; nftContract = pictureData.nftContract; nftID = pictureData.nftID; uint256 cidNFTID = cidNFT.getPrimaryCIDNFT(subprotocolName, _pfpID); IAddressRegistry addressRegistry = cidNFT.addressRegistry(); if (cidNFTID == 0 || addressRegistry.getAddress(cidNFTID) != ERC721(nftContract).ownerOf(nftID)) { nftContract = address(0); nftID = 0; // Strictly not needed because nftContract has to be always checked, but reset nevertheless to 0 } }
Consider the following scenario.
function getAddress(uint256 _cidNFTID) external view returns (address user) { user = ERC721(cidNFT).ownerOf(_cidNFTID); if (_cidNFTID != cidNFTs[user]) { // User owns CID NFT, but has not registered it user = address(0); } }
alice sends the underlying NFT to bob, who does what alice did before
bob sold the underlying NFT to charlie and charlie burned it for some reason, now if alice or bob calls ProfilePicture.getPFP(), it will return the contract address and id of the underlying NFT since getAddress() == ownerOf() == address(0).
When integrating with other projects, there may be bugs because getPFP returns an incorrect result
https://github.com/code-423n4/2023-03-canto-identity/blob/077372297fc419ea7688ab62cc3fd4e8f4e24e66/canto-pfp-protocol/src/ProfilePicture.sol#L94-L105 https://github.com/code-423n4/2023-03-canto-identity/blob/077372297fc419ea7688ab62cc3fd4e8f4e24e66/canto-identity-protocol/src/AddressRegistry.sol#L86-L92
None
Change to
function getPFP(uint256 _pfpID) public view returns (address nftContract, uint256 nftID) { if (_ownerOf[_pfpID] == address(0)) revert TokenNotMinted(_pfpID); ProfilePictureData storage pictureData = pfp[_pfpID]; nftContract = pictureData.nftContract; nftID = pictureData.nftID; uint256 cidNFTID = cidNFT.getPrimaryCIDNFT(subprotocolName, _pfpID); IAddressRegistry addressRegistry = cidNFT.addressRegistry(); - if (cidNFTID == 0 || addressRegistry.getAddress(cidNFTID) != ERC721(nftContract).ownerOf(nftID)) { + if (cidNFTID == 0 || ERC721(nftContract).ownerOf(nftID) == 0 || addressRegistry.getAddress(cidNFTID) != ERC721(nftContract).ownerOf(nftID)) { nftContract = address(0); nftID = 0; // Strictly not needed because nftContract has to be always checked, but reset nevertheless to 0 } }
#0 - OpenCoreCH
2023-03-29T20:33:29Z
Very interesting edge case, enjoyed reading it! For an ERC721 compliant NFT this is not the case, because the standard explicitly says:
/// @dev NFTs assigned to zero address are considered invalid, and queries /// about them do throw.
Therefore, the existing ownerOf
will throw.
However, in my experience there are some NFTs that do not adhere strictly to ERC721 when it comes to this. Because of that, I'll still implement a fix for this.
#1 - c4-sponsor
2023-03-29T20:33:38Z
OpenCoreCH marked the issue as sponsor confirmed
#2 - c4-judge
2023-04-10T14:31:51Z
0xleastwood marked the issue as primary issue
#3 - c4-judge
2023-04-11T19:56:31Z
0xleastwood marked the issue as selected for report