Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 14 additions & 38 deletions contracts/extension/ContractMetadata.sol
Original file line number Diff line number Diff line change
@@ -1,48 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
pragma solidity ^0.8.19;

/// @author thirdweb
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

import "./interface/IContractMetadata.sol";
contract RebelByNature is ERC721URIStorage, Ownable, ContractMetadata {
Copy link

@coderabbitai coderabbitai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

ContractMetadata is undefined—missing import/definition.

The contract inherits ContractMetadata, but that type is no longer declared in this file nor imported from elsewhere, so the compiler cannot resolve it. Reintroduce the original extension declaration here or import the correct contract before using it.

🤖 Prompt for AI Agents
In contracts/extension/ContractMetadata.sol around line 7, the contract inherits
ContractMetadata which is not defined or imported; add the missing declaration
or import the correct ContractMetadata contract before using it. Restore the
original ContractMetadata extension by either reintroducing its
contract/interface implementation in this file or adding an import statement
that points to the file that declares ContractMetadata, and then ensure the
inheritance list compiles (update SPDX/pragma or paths as needed).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./ContractMetadata.sol";

contract RebelByNature is ERC721URIStorage, Ownable, ContractMetadata {
uint256 public nextTokenId;

event TributeMinted(uint256 tokenId, address recipient, uint256 timestamp);

constructor() ERC721("Rebel By Nature", "RBN") {}

function mintTribute(address recipient) external onlyOwner {
    uint256 tokenId = nextTokenId++;
    _mint(recipient, tokenId);
    _setTokenURI(tokenId, contractURI);
    emit TributeMinted(tokenId, recipient, block.timestamp);
}

function _canSetContractURI() internal view override returns (bool) {
    return msg.sender == owner();
}

}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for ContractMetadata contract or interface definitions
echo "=== Searching for ContractMetadata definitions ==="
rg -nP --type=sol -C3 '^\s*(contract|interface|abstract contract)\s+ContractMetadata\b'

echo -e "\n=== Searching for IContractMetadata interface ==="
rg -nP --type=sol -C3 '^\s*interface\s+IContractMetadata\b'

echo -e "\n=== Check the interface directory ==="
fd -e sol . contracts/extension/interface/

echo -e "\n=== Git history of ContractMetadata.sol (before PR) ==="
git show main:contracts/extension/ContractMetadata.sol | head -50

Length of output: 4558


🏁 Script executed:

#!/bin/bash
# Get the IContractMetadata interface content
echo "=== IContractMetadata interface content ==="
cat contracts/extension/interface/IContractMetadata.sol

echo -e "\n=== Check if RebelByNature.sol already exists ==="
fd -e sol "RebelByNature" contracts/

Length of output: 930


@sonnyquinn24, the import import "./ContractMetadata.sol"; creates a self-import (the file importing itself), which will cause a circular dependency compilation error.

The Problem

You've completely replaced the ContractMetadata.sol file, which originally contained an abstract contract extension, with your RebelByNature contract. The file cannot import itself.

Recommended Solution: Separate Files

Restore contracts/extension/ContractMetadata.sol to its original content and create a new file for your contract:

contracts/extension/ContractMetadata.sol (restore original):

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import "./interface/IContractMetadata.sol";

abstract contract ContractMetadata is IContractMetadata {
    error ContractMetadataUnauthorized();

    string public override contractURI;

    function setContractURI(string memory _uri) external override {
        if (!_canSetContractURI()) {
            revert ContractMetadataUnauthorized();
        }
        _setupContractURI(_uri);
    }

    function _setupContractURI(string memory _uri) internal {
        string memory prevURI = contractURI;
        contractURI = _uri;
        emit ContractURIUpdated(prevURI, _uri);
    }

    function _canSetContractURI() internal view virtual returns (bool);
}

contracts/RebelByNature.sol (new file):

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./extension/ContractMetadata.sol";

contract RebelByNature is ERC721URIStorage, Ownable, ContractMetadata {
    uint256 public nextTokenId;

    event TributeMinted(uint256 tokenId, address recipient, uint256 timestamp);

    constructor() ERC721("Rebel By Nature", "RBN") Ownable(msg.sender) {}

    function mintTribute(address recipient) external onlyOwner {
        uint256 tokenId = nextTokenId++;
        _mint(recipient, tokenId);
        _setTokenURI(tokenId, contractURI);
        emit TributeMinted(tokenId, recipient, block.timestamp);
    }

    function _canSetContractURI() internal view override returns (bool) {
        return msg.sender == owner();
    }
}

Note: I also added Ownable(msg.sender) to the constructor, which is required for OpenZeppelin's Ownable in v5.x if you're using that version.

Alternative: Combined File (Not Recommended)

If you prefer a single file, include both contract definitions:

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interface/IContractMetadata.sol";

abstract contract ContractMetadata is IContractMetadata {
    error ContractMetadataUnauthorized();
    string public override contractURI;

    function setContractURI(string memory _uri) external override {
        if (!_canSetContractURI()) {
            revert ContractMetadataUnauthorized();
        }
        _setupContractURI(_uri);
    }

    function _setupContractURI(string memory _uri) internal {
        string memory prevURI = contractURI;
        contractURI = _uri;
        emit ContractURIUpdated(prevURI, _uri);
    }

    function _canSetContractURI() internal view virtual returns (bool);
}

contract RebelByNature is ERC721URIStorage, Ownable, ContractMetadata {
    uint256 public nextTokenId;

    event TributeMinted(uint256 tokenId, address recipient, uint256 timestamp);

    constructor() ERC721("Rebel By Nature", "RBN") Ownable(msg.sender) {}

    function mintTribute(address recipient) external onlyOwner {
        uint256 tokenId = nextTokenId++;
        _mint(recipient, tokenId);
        _setTokenURI(tokenId, contractURI);
        emit TributeMinted(tokenId, recipient, block.timestamp);
    }

    function _canSetContractURI() internal view override returns (bool) {
        return msg.sender == owner();
    }
}

The separate files approach is strongly recommended to maintain the repository's extension architecture.

uint256 public nextTokenId;

/**
* @title Contract Metadata
* @notice Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI
* for you contract.
* Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea.
*/
event TributeMinted(uint256 tokenId, address recipient, uint256 timestamp);

abstract contract ContractMetadata is IContractMetadata {
/// @dev The sender is not authorized to perform the action
error ContractMetadataUnauthorized();
constructor() ERC721("Rebel By Nature", "SEQREB") {}

/// @notice Returns the contract metadata URI.
string public override contractURI;

/**
* @notice Lets a contract admin set the URI for contract-level metadata.
* @dev Caller should be authorized to setup contractURI, e.g. contract admin.
* See {_canSetContractURI}.
* Emits {ContractURIUpdated Event}.
*
* @param _uri keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
*/
function setContractURI(string memory _uri) external override {
if (!_canSetContractURI()) {
revert ContractMetadataUnauthorized();
}

_setupContractURI(_uri);
function mintTribute(address recipient) external onlyOwner {
uint256 tokenId = nextTokenId++;
_mint(recipient, tokenId);
_setTokenURI(tokenId, contractURI);
emit TributeMinted(tokenId, recipient, block.timestamp);
Comment on lines +14 to +18
Copy link

@coderabbitai coderabbitai bot Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix compile error and mint safety: call contractURI(), use _safeMint

  • _setTokenURI(tokenId, contractURI); likely fails: contractURI is a function in the extension; call it as contractURI().
  • Prefer _safeMint to avoid sending tokens to non-receiver contracts.

Apply:

-    function mintTribute(address recipient) external onlyOwner {
-        uint256 tokenId = nextTokenId++;
-        _mint(recipient, tokenId);
-        _setTokenURI(tokenId, contractURI);
-        emit TributeMinted(tokenId, recipient, block.timestamp);
-    }
+    function mintTribute(address recipient) external onlyOwner {
+        uint256 tokenId = nextTokenId++;
+        _safeMint(recipient, tokenId);
+        _setTokenURI(tokenId, contractURI());
+        emit TributeMinted(tokenId, recipient, block.timestamp);
+    }

Optional: accept a per‑token URI to avoid copying the contract‑level URI into token metadata.

function mintTribute(address recipient, string calldata uri) external onlyOwner {
    uint256 tokenId = nextTokenId++;
    _safeMint(recipient, tokenId);
    _setTokenURI(tokenId, uri);
    emit TributeMinted(tokenId, recipient, block.timestamp);
}
🤖 Prompt for AI Agents
In contracts/extension/ContractMetadata.sol around lines 15 to 19, the
mintTribute implementation currently calls contractURI as if it were a variable
and uses _mint which can send tokens to non-receiver contracts; change the call
to contractURI() if you intend to use the contract-level URI, and replace _mint
with _safeMint to ensure ERC721Receiver checks. Optionally, change the function
signature to accept a per-token URI (e.g., mintTribute(address recipient, string
calldata uri)), call _safeMint(recipient, tokenId), then _setTokenURI(tokenId,
uri) (or _setTokenURI(tokenId, contractURI()) if you want the contract-level
URI), and emit the event as before.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}

/// @dev Lets a contract admin set the URI for contract-level metadata.
function _setupContractURI(string memory _uri) internal {
string memory prevURI = contractURI;
contractURI = _uri;

emit ContractURIUpdated(prevURI, _uri);
function _canSetContractURI() internal view override returns (bool) {
return msg.sender == owner();
}

/// @dev Returns whether contract metadata can be set in the given execution context.
function _canSetContractURI() internal view virtual returns (bool);
}