Sample ERC721 Contract
This is an example of an ERC-721 smart contract that mints unique NFT tokens and transfers them to the specified owner address.
This example is adapted from OpenZeppelin ERC721.
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import "@openzeppelin-contracts/access/Ownable.sol"; // Module for ownership control (e.g., onlyOwner)
import "@openzeppelin-contracts/token/ERC721/utils/ERC721Utils.sol"; // Utility for verifying ERC721 receiver on safeTransferFrom
import "@openzeppelin-contracts/utils/Strings.sol"; // String utility for converting uint256 to string
/**
* @title Sample721Token
* @dev A custom ERC721 contract implementing minimal functionalities
* - mint and burn can only be performed by the owner
* - implements basic approve, transferFrom, safeTransferFrom
* - Uses OpenZeppelin's Ownable to grant ownership privileges to the deployer
*/
contract Sample721Token is Ownable {
using Strings for uint256;
// ----------------------
// ░░░ Error Definitions ░░░
// ----------------------
error ERC721InvalidOwner(address owner); // Invalid owner address
error ERC721NonexistentToken(uint256 tokenId); // Nonexistent token
error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); // Not the owner
error ERC721InvalidSender(address sender); // Invalid sender
error ERC721InvalidReceiver(address receiver); // Invalid receiver
error ERC721TokenAlreadyMinted(uint256 tokenId); // Token already minted
error ERC721NotAuthorized(address operator, uint256 tokenId); // Unauthorized
error ERC721InsufficientApproval(address operator, uint256 tokenId); // Insufficient approval
error ERC721InvalidApprover(address approver); // Invalid approver
error ERC721InvalidOperator(address operator); // Invalid operator
// ----------------------
// ░░░ Event Definitions ░░░
// ----------------------
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); // Transfer event
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); // Approval event
event ApprovalForAll(address indexed owner, address indexed operator, bool approved); // Approval for all event
event MetadataUpdate(uint256 _tokenId); // Metadata update notification
event BaseURIChanged(string newBaseURI); // Base URI change notification
event TokenMinted(address indexed to, uint256 indexed tokenId); // Token mint event
// ----------------------
// ░░░ State Variables ░░░
// ----------------------
string public name; // NFT name
string public symbol; // NFT symbol
string private _baseURI; // Base metadata URI
mapping(uint256 tokenId => string) private _tokenURIs; // Individual token URIs
uint256 private _nextTokenId; // Next token ID to mint
mapping(uint256 => address) private _owners; // Token ownership mapping
mapping(address => uint256) private _balances; // Token balance per address
mapping(uint256 => address) private _tokenApprovals; // Token-level approval mapping
mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; // Operator approvals
// ----------------------
// ░░░ Constructor ░░░
// ----------------------
/**
* @param name_ NFT collection name
* @param symbol_ NFT collection symbol
* @param baseURI Base metadata URI
*/
constructor(string memory name_, string memory symbol_, string memory baseURI) Ownable(msg.sender) {
name = name_;
symbol = symbol_;
_baseURI = baseURI;
}
// ----------------------
// ░░░ ERC721 Standard Functions ░░░
// ----------------------
/// @notice Returns the number of tokens owned by a given address
/// @param owner The address of the token owner
/// @return The number of tokens owned
function balanceOf(address owner) external view returns (uint256) {
if (owner == address(0)) {
revert ERC721InvalidOwner(address(0));
}
return _balances[owner];
}
/// @notice Returns the address of the owner of a specific token
/// @param tokenId The ID of the token to query
/// @return The address of the owner
function ownerOf(uint256 tokenId) external view returns (address) {
return _requireOwned(tokenId);
}
/// @notice Grants approval to a specific address for a specific token
/// @param to The address to be approved
/// @param tokenId The ID of the token to approve
function approve(address to, uint256 tokenId) external {
_approve(to, tokenId, msg.sender, true);
}
/// @dev Internal function to handle approval logic
/// @param to The address to be approved
/// @param tokenId The token ID to approve
/// @param auth The caller (for permission check)
/// @param emitEvent Whether to emit the Approval event
function _approve(address to, uint256 tokenId, address auth, bool emitEvent) internal {
if (emitEvent || auth != address(0)) {
address owner = _requireOwned(tokenId);
// Reject if the caller is not authorized to approve
if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
revert ERC721InvalidApprover(auth);
}
if (emitEvent) {
emit Approval(owner, to, tokenId);
}
}
_tokenApprovals[tokenId] = to;
}
/// @notice Returns the approved address for a specific token
/// @param tokenId The token ID to query
/// @return The approved address
function getApproved(uint256 tokenId) external view returns (address) {
if (_owners[tokenId] == address(0)) revert ERC721NonexistentToken(tokenId);
return _tokenApprovals[tokenId];
}
/// @notice Sets or unsets operator approval for all of the caller's tokens
/// @param operator The address to be approved as operator
/// @param approved Whether the approval is granted (true) or revoked (false)
function setApprovalForAll(address operator, bool approved) external {
address owner = msg.sender;
if (operator == address(0)) {
revert ERC721InvalidOperator(operator);
}
_operatorApprovals[owner][operator] = approved;
emit ApprovalForAll(owner, operator, approved);
}
/// @notice Checks if an operator is approved to manage all tokens of a specific owner
/// @param owner The address of the token owner
/// @param operator The address to check for operator approval
/// @return Whether the operator is approved
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
/// @notice Transfers a token to another address if the caller is authorized
/// @param from The current owner's address
/// @param to The recipient's address
/// @param tokenId The token ID to transfer
function transferFrom(address from, address to, uint256 tokenId) public {
if (to == address(0)) revert ERC721InvalidReceiver(address(0));
// Check authorization and perform state update
address previousOwner = _update(to, tokenId, msg.sender);
if (previousOwner != from) {
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
}
}
/// @notice Safe transfer: includes ERC721 receiver interface check if recipient is a contract
/// @param from The current owner's address
/// @param to The recipient's address
/// @param tokenId The token ID to transfer
function safeTransferFrom(address from, address to, uint256 tokenId) external {
safeTransferFrom(from, to, tokenId, "");
}
/// @notice Safe transfer with additional data: includes ERC721 receiver interface check if recipient is a contract
/// @param from The current owner's address
/// @param to The recipient's address
/// @param tokenId The token ID to transfer
/// @param data Additional data to include with the transfer
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public {
transferFrom(from, to, tokenId);
ERC721Utils.checkOnERC721Received(msg.sender, from, to, tokenId, data);
}
/// @notice Returns whether the contract supports a given interface ID (ERC165)
/// @param interfaceId The interface identifier
/// @return Whether the interface is supported
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return
interfaceId == 0x01ffc9a7 || // ERC165
interfaceId == 0x80ac58cd || // ERC721
interfaceId == 0x5b5e139f; // ERC721Metadata
}
/// @notice Returns the metadata URI of a specific token
/// @param tokenId The ID of the token
/// @return The metadata URI of the token
function tokenURI(uint256 tokenId) external view returns (string memory) {
_requireOwned(tokenId);
string memory _tokenURI = _tokenURIs[tokenId];
string memory base = _baseURI;
if (bytes(base).length == 0) {
return _tokenURI;
}
if (bytes(_tokenURI).length > 0) {
return string.concat(base, _tokenURI);
}
return bytes(base).length > 0 ? string.concat(base, tokenId.toString()) : "";
}
/// @notice Sets the metadata URI for a specific token
/// @param tokenId The ID of the token
/// @param _tokenURI The metadata URI string
function setTokenURI(uint256 tokenId, string memory _tokenURI) external onlyOwner {
_tokenURIs[tokenId] = _tokenURI;
emit MetadataUpdate(tokenId);
}
/// @notice Sets the base URI for all token metadata
/// @param baseURI The base URI string
function setBaseURI(string memory baseURI) external onlyOwner {
_baseURI = baseURI;
emit BaseURIChanged(baseURI);
}
// ----------------------
// ░░░ Owner-Only Functions (Mint/Burn) ░░░
// ----------------------
/// @notice Minting function that can only be called by the contract owner. Mints a token and emits an event.
/// @param to The address that will receive the token
function mint(address to) external onlyOwner {
uint256 tokenId = ++_nextTokenId;
_mint(to, tokenId);
emit TokenMinted(to, tokenId);
}
/// @notice Publicly callable safe minting function. Verifies receiver if it's a contract.
/// @param to The address that will receive the token
function safeMint(address to) external {
uint256 tokenId = ++_nextTokenId;
_safeMint(to, tokenId, "");
}
/// @dev Internally called safe minting function. Verifies that receiver implements the ERC721 receiver interface if it's a contract.
/// @param to The address that will receive the token
/// @param tokenId The token ID to mint
/// @param data Additional data (usually an empty value)
function _safeMint(address to, uint256 tokenId, bytes memory data) internal {
_mint(to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), address(0), to, tokenId, data);
}
/// @notice Burns the token if the caller is authorized
/// @dev Uses _update to set ownership to address(0), effectively burning it. Emits a Transfer event.
/// @param tokenId The ID of the token to burn
function burn(uint256 tokenId) external {
// _update handles authorization, ownership validation, state update, and event emission
address previousOwner = _update(address(0), tokenId, _msgSender());
// Revert if the token doesn't exist
if (previousOwner == address(0)) {
revert ERC721NonexistentToken(tokenId);
}
}
// ----------------------
// ░░░ Internal Transfer Logic ░░░
// ----------------------
/// @dev Mints a new token. Reverts if the token already exists.
/// @param to The address that will receive the token
/// @param tokenId The token ID to mint
function _mint(address to, uint256 tokenId) internal {
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
// Call _update to assign the token to 'to'
address previousOwner = _update(to, tokenId, address(0));
// Revert if the token already exists
if (previousOwner != address(0)) {
revert ERC721InvalidSender(address(0));
}
}
/// @dev Transfers an existing token to another address. Requires that 'from' is the actual owner.
/// @param from The current owner of the token
/// @param to The recipient address
/// @param tokenId The token ID to transfer
function _transfer(address from, address to, uint256 tokenId) internal {
if (to == address(0)) {
revert ERC721InvalidReceiver(address(0));
}
// Perform transfer without checking auth (used for internal calls)
address previousOwner = _update(to, tokenId, address(0));
// Revert if token doesn't exist
if (previousOwner == address(0)) {
revert ERC721NonexistentToken(tokenId);
}
// Revert if 'from' is not the actual owner
else if (previousOwner != from) {
revert ERC721IncorrectOwner(from, tokenId, previousOwner);
}
}
/// @dev Performs a safe transfer without any additional data
/// @param from The current owner of the token
/// @param to The recipient address
/// @param tokenId The token ID to transfer
function _safeTransfer(address from, address to, uint256 tokenId) internal {
_safeTransfer(from, to, tokenId, "");
}
/// @dev Performs a safe transfer and checks if the recipient is a contract that implements the ERC721 receiver interface
/// @param from The current owner of the token
/// @param to The recipient address
/// @param tokenId The token ID to transfer
/// @param data Additional data
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal {
_transfer(from, to, tokenId);
ERC721Utils.checkOnERC721Received(_msgSender(), from, to, tokenId, data);
}
/// @dev Updates the ownership information of a token and emits a Transfer event
/// @param to The new owner of the token
/// @param tokenId The ID of the token to update
/// @param auth The caller's address used for authorization checks (skip check if 0)
/// @return from The previous owner's address
function _update(address to, uint256 tokenId, address auth) internal returns (address) {
address from = _owners[tokenId];
// If auth is provided, perform authorization check
if (auth != address(0)) {
if (!_isAuthorized(from, auth, tokenId)) {
if (from == address(0)) {
revert ERC721NonexistentToken(tokenId);
} else {
revert ERC721InsufficientApproval(auth, tokenId);
}
}
}
// If there was a previous owner, decrease balance and remove approval
if (from != address(0)) {
_approve(address(0), tokenId, address(0), false);
unchecked {
_balances[from] -= 1;
}
}
// If there is a new owner, increase balance
if (to != address(0)) {
unchecked {
_balances[to] += 1;
}
}
// Update ownership
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
return from;
}
/// @dev Checks whether the caller is an authorized operator for the given token
/// @param owner The current owner of the token
/// @param spender The address attempting to act on the token
/// @param tokenId The token ID in question
/// @return true if authorized, false otherwise
function _isAuthorized(address owner, address spender, uint256 tokenId) internal view returns (bool) {
return
spender != address(0) &&
(owner == spender || isApprovedForAll(owner, spender) || _tokenApprovals[tokenId] == spender);
}
/// @dev Ensures the token exists and returns its owner address
/// @param tokenId The token ID to check
/// @return owner The address of the token owner
function _requireOwned(uint256 tokenId) internal view returns (address) {
address owner = _owners[tokenId];
if (owner == address(0)) {
revert ERC721NonexistentToken(tokenId);
}
return owner;
}
}
Updated 14 days ago