Exorium Guide
  • 🟒Introduction
  • ⚫Architecture
  • 🟒Governance Model
    • Treasury & Resource Allocation
    • Benefits of Decentralized Governance
    • Future Governance Enhancements
  • ⚫Validator
  • 🟒Tokenomics
    • Utility & Use Cases
    • Staking & Reward Mechanisms
    • Sustainability & Growth
  • Getting Started
    • ⚫Getting Started
    • 🟒Setup Exorium Network
    • ⚫Consensus Engine
    • 🟒Genesis File
      • Example of a Minimal Genesis File
      • Maintenance & Version Control
    • ⚫Contributing
    • 🟒Smart Contract
    • ⚫ERC721
  • 🟒Roadmap
  • Staking Guide
    • Formula for APY
Powered by GitBook
On this page
  1. Getting Started

ERC721

Below is a sample ERC-721 (NFT) smart contract in Solidity that you can use as a reference for minting and managing Non-Fungible Tokens (NFTs) on the Exorium network (or any EVM-compatible chain). This example uses the basic ERC-721 standard, which you can further enhance with advanced features such as metadata URIs, royalties, pausable functionality, or on-chain governance.

Note:

  • Make sure you have added the Exorium Network to your wallet (e.g MetaMask) or development suite (Hardhat/Truffle/Remix).

  • Use the Exorium Testnet for initial testing, then deploy to Mainnet once everything has been audited and thoroughly tested.

  • Always follow best practices, such as using audited libraries (like OpenZeppelin), especially for projects involving real user assets.

Sample ERC-721 (NFT) Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
 * @title ExoriumNFT
 * @dev A simple ERC-721 token example on the Exorium Network.
 */

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(
        address indexed owner,
        address indexed approved,
        uint256 indexed tokenId
    );
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );

    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId)
        external
        view
        returns (address operator);

    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator)
        external
        view
        returns (bool);

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

interface IERC721Receiver {
    function onERC721Received(
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
    ) external returns (bytes4);
}

interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function tokenURI(uint256 tokenId) external view returns (string memory);
}

contract ExoriumNFT is IERC721Metadata {
    // Token name
    string private _name;

    // Token symbol
    string private _symbol;

    // Mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping(address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping(uint256 => address) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // Base URI for retrieving token metadata
    string private _baseURI;

    // Token counter for minting new NFTs
    uint256 private _currentTokenId;

    /**
     * @dev Constructor that sets the name and symbol.
     * Optionally, you can set an initial baseURI for metadata.
     */
    constructor(string memory name_, string memory symbol_, string memory baseURI_) {
        _name = name_;
        _symbol = symbol_;
        _baseURI = baseURI_;
    }

    // --------------------------- ERC165 ---------------------------

    function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
        // ERC165 Interface ID for ERC721: 0x80ac58cd
        // ERC165 Interface ID for ERC721Metadata: 0x5b5e139f
        return
            interfaceId == 0x80ac58cd ||
            interfaceId == 0x5b5e139f ||
            interfaceId == 0x01ffc9a7;
    }

    // -------------------------- ERC721 ----------------------------

    function balanceOf(address owner) public view override returns (uint256 balance) {
        require(owner != address(0), "ExoriumNFT: balance query for the zero address");
        return _balances[owner];
    }

    function ownerOf(uint256 tokenId) public view override returns (address owner) {
        owner = _owners[tokenId];
        require(owner != address(0), "ExoriumNFT: owner query for nonexistent token");
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external override {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) public override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ExoriumNFT: caller is not token owner nor approved");
        _transfer(from, to, tokenId);
        require(
            _checkOnERC721Received(from, to, tokenId, data),
            "ExoriumNFT: transfer to non ERC721Receiver implementer"
        );
    }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public override {
        require(_isApprovedOrOwner(msg.sender, tokenId), "ExoriumNFT: caller is not token owner nor approved");
        _transfer(from, to, tokenId);
    }

    function approve(address to, uint256 tokenId) public override {
        address owner = ownerOf(tokenId);
        require(to != owner, "ExoriumNFT: approval to current owner");
        require(
            msg.sender == owner || isApprovedForAll(owner, msg.sender),
            "ExoriumNFT: caller is not owner nor approved for all"
        );

        _approve(to, tokenId);
    }

    function getApproved(uint256 tokenId) public view override returns (address operator) {
        require(_exists(tokenId), "ExoriumNFT: approved query for nonexistent token");
        return _tokenApprovals[tokenId];
    }

    function setApprovalForAll(address operator, bool approved) public override {
        require(operator != msg.sender, "ExoriumNFT: approve to caller");
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    function isApprovedForAll(address owner, address operator)
        public
        view
        override
        returns (bool)
    {
        return _operatorApprovals[owner][operator];
    }

    // ------------------------ ERC721 Metadata ----------------------

    function name() public view override returns (string memory) {
        return _name;
    }

    function symbol() public view override returns (string memory) {
        return _symbol;
    }

    /**
     * @dev Returns the URI for a given token ID. If a `baseURI` is set,
     * the final URI is `baseURI + tokenId` (converted to string).
     */
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_exists(tokenId), "ExoriumNFT: URI query for nonexistent token");
        return string(abi.encodePacked(_baseURI, _uint2str(tokenId), ".json"));
    }

    // ------------------------ Minting Logic ------------------------

    /**
     * @dev Creates a new token for `to`. The token ID is incremented automatically.
     * Only the contract owner (or a designated minter) can call this (if you want restricted access).
     * Otherwise, make this public if anyone can mint.
     */
    function mint(address to) public returns (uint256) {
        require(to != address(0), "ExoriumNFT: mint to the zero address");

        _currentTokenId++;
        uint256 newTokenId = _currentTokenId;
        _mint(to, newTokenId);
        return newTokenId;
    }

    // ------------------------ Internal Functions -------------------

    function _transfer(
        address from,
        address to,
        uint256 tokenId
    ) internal {
        require(ownerOf(tokenId) == from, "ExoriumNFT: transfer of token that is not own");
        require(to != address(0), "ExoriumNFT: transfer to the zero address");

        // Clear approvals
        _approve(address(0), tokenId);

        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function _mint(address to, uint256 tokenId) internal {
        require(!_exists(tokenId), "ExoriumNFT: token already minted");

        _owners[tokenId] = to;
        _balances[to] += 1;

        emit Transfer(address(0), to, tokenId);
    }

    function _approve(address to, uint256 tokenId) internal {
        _tokenApprovals[tokenId] = to;
        emit Approval(ownerOf(tokenId), to, tokenId);
    }

    function _exists(uint256 tokenId) internal view returns (bool) {
        return _owners[tokenId] != address(0);
    }

    /**
     * @dev Returns whether `spender` is allowed to manage `tokenId`.
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId)
        internal
        view
        returns (bool)
    {
        require(_exists(tokenId), "ExoriumNFT: operator query for nonexistent token");
        address owner = ownerOf(tokenId);
        return (spender == owner ||
            getApproved(tokenId) == spender ||
            isApprovedForAll(owner, spender));
    }

    /**
     * @dev Checks if `to` is a contract and if so, calls `onERC721Received`.
     */
    function _checkOnERC721Received(
        address from,
        address to,
        uint256 tokenId,
        bytes memory data
    ) private returns (bool) {
        if (_isContract(to)) {
            try
                IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data)
            returns (bytes4 retval) {
                return retval == IERC721Receiver.onERC721Received.selector;
            } catch (bytes memory reason) {
                if (reason.length == 0) {
                    revert("ExoriumNFT: transfer to non ERC721Receiver implementer");
                } else {
                    // Bubble up the revert reason
                    assembly {
                        revert(add(32, reason), mload(reason))
                    }
                }
            }
        } else {
            return true;
        }
    }

    /**
     * @dev Check if address is a contract. 
     */
    function _isContract(address addr) private view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(addr)
        }
        return size > 0;
    }

    // Helper function to convert uint to string
    function _uint2str(uint256 value) private pure returns (string memory) {
        if (value == 0) {
            return "0";
        }
        uint256 temp = value;
        uint256 digits;
        while (temp != 0) {
            digits++;
            temp /= 10;
        }
        bytes memory buffer = new bytes(digits);
        while (value != 0) {
            digits -= 1;
            buffer[digits] = bytes1(uint8(48 + (value % 10)));
            value /= 10;
        }
        return string(buffer);
    }
}

Overview of Key Features

  1. ERC-721 Interfaces

    • Implements standard interfaces (IERC165, IERC721, IERC721Metadata) for basic NFT functionality (transfer, approve, balance checks, etc.) and metadata (name, symbol, tokenURI).

  2. Minting Logic

    • The mint function increments _currentTokenId and creates a new token for the specified address.

    • You can restrict access to mint by adding onlyOwner or a similar modifier if desired.

  3. Metadata

    • A _baseURI is stored in the constructor, allowing you to set a base path for your off-chain metadata files.

    • tokenURI concatenates the base URI with the tokenId, assuming the metadata file follows a pattern like baseURI/1.json, baseURI/2.json, etc.

  4. Transfers

    • transferFrom and safeTransferFrom handle moving tokens between addresses.

    • Safe transfers also call _checkOnERC721Received to ensure compatibility with contract-based receivers.

  5. Approvals & Operators

    • Token owners can approve a specific address to manage a single token or set an operator to manage all tokens on their behalf.

  6. SupportsInterface

    • Uses the standard ERC165 mechanism for interface detection.

Deployment Steps on Exorium

  1. Network Setup

    • In your wallet or dev environment (Remix, Hardhat, Truffle), add Exorium parameters (RPC URL, Chain ID, etc.).

  2. Compile & Deploy

    • If using Remix, paste the code above into a .sol file.

    • Set the compiler to a matching Solidity version (e.g 0.8.x).

    • In β€œDeploy & Run,” select Injected Web3 (for MetaMask or another Web3 provider), ensuring you are connected to the Exorium network.

    • Provide the constructor arguments (e.g ExoriumNFT, EXNFT, https://mydomain.com/metadata/) if using Remix.

    • Click Deploy and confirm the transaction.

  3. Verification

    • Go to the Exorium block explorer (e.g Exoscan), find your contract, and follow the verification instructions if available to make source code public.

  4. Testing Mints

    • Call the mint function (optionally restricted to a contract owner) to mint new NFTs to your address or a friend address.

    • Use ownerOf(tokenId) to confirm ownership.

    • Check tokenURI(tokenId) to ensure it resolves to your base metadata URL.

Tips & Best Practices

  • Use OpenZeppelin Libraries

    • For production-grade code, consider importing OpenZeppelin ERC721 implementation to benefit from audited, widely used standards.

  • Access Control

    • If you only want specific accounts to mint, add an ownable pattern (onlyOwner or a role-based system) to mint.

  • Metadata Storage

    • Decide if your metadata is centralized, decentralized (IPFS/Arweave), or on-chain. Modify tokenURI accordingly.

  • Batch Minting & Airdrops

    • If you plan to mint multiple NFTs in one transaction, you can create a loop or a specialized function.

  • Advanced Features

    • Royalty standard (EIP-2981), lazy minting, or layered metadata can be added depending on your use case.

This ERC-721 NFT sample contract provides a straightforward way to create NFTs on the Exorium Network (or any EVM-compatible blockchain). By customizing functions like mint and tokenURI, you can adapt it to collectibles, gaming items, or any other NFT application you envision. Once deployed and verified, users can view and trade your NFTs with any wallet or marketplace that supports the ERC-721 standard. Happy minting!

PreviousSmart ContractNextRoadmap

Last updated 4 months ago

⚫