Canto Identity Subprotocols contest - cccz's results

Subprotocols for Canto Identity Protocol.

General Information

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

Canto Identity Subprotocols

Findings Distribution

Researcher Performance

Rank: 3/98

Findings: 1

Award: $3,269.95

🌟 Selected for report: 1

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: cccz

Labels

bug
2 (Med Risk)
primary issue
selected for report
sponsor confirmed
edited-by-warden
M-05

Awards

3269.9461 USDC - $3,269.95

External Links

Lines of code

https://github.com/code-423n4/2023-03-canto-identity/blob/077372297fc419ea7688ab62cc3fd4e8f4e24e66/canto-pfp-protocol/src/ProfilePicture.sol#L94-L105

Vulnerability details

Impact

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.

  1. alice mint a ProfilePicture NFT (pfp NFT) with the underlying NFT, and later mint a CidNFT by transferring the pfp NFT to the CidNFT contract. noting that alice does not call AddressRegistry.register to register the CidNFT. Since getAddress returns 0, ProfilePicture.getPFP will also return 0
    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);
        }
    }
  1. alice sends the underlying NFT to bob, who does what alice did before

  2. 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

Proof of Concept

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

Tools Used

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter