Created
July 12, 2025 05:56
-
-
Save ShariqKhan2012/d06b0eb08e712b4164259aa444c34222 to your computer and use it in GitHub Desktop.
This gist contains an implementation of EIP-ERC721 (NFT) standard. It also implements the required ERC165 (supportsInterface) standard and the optional ERC721Enumerable, and ERC721Metadata extensions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //SPDX-License-Identifier: MIT | |
| pragma solidity ^0.8.24; | |
| interface IERC165 { | |
| function supportsInterface(bytes4 interfaceId) external pure returns (bool); | |
| } | |
| /* ERC721 MUST implement ERC165 */ | |
| interface IERC721 is IERC165 { | |
| event Transfer(address indexed from, address indexed to, uint256 tokenId); | |
| event Approval(address indexed owner, address indexed spender, uint256 tokenId); | |
| event ApprovalForAll(address indexed owner, address indexed operator, bool approved); | |
| //SIGBOATS | |
| function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) external payable; | |
| function safeTransferFrom(address from, address to, uint256 tokenId) external payable; | |
| function transferFrom(address from, address to, uint256 tokenId) external payable; | |
| function isApprovedForAll(address owner, address operator) external returns (bool); | |
| function getApproved(uint256 tokenId) external returns (address); | |
| function balanceOf(address owner) external returns (uint256); | |
| function ownerOf(uint256 tokenId) external returns (address); | |
| function approve(address spender, uint256 tokenId) external payable; | |
| function setApprovalForAll(address operator, bool approved) external; | |
| } | |
| /** | |
| * ERC721Metadata SHALL automatically implement ERC721 | |
| * because it does not stand alone without the ERC721 | |
| */ | |
| 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); | |
| } | |
| /** | |
| * IERC721Enumerable SHALL automatically implement ERC721 | |
| * because it does not stand alone without the ERC721 | |
| */ | |
| interface IERC721Enumerable is IERC721 { | |
| function totalSupply() external view returns (uint256); | |
| function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); | |
| function tokenByIndex(uint256 index) external view returns (uint256); | |
| } | |
| /** | |
| * IERC721Receiver does NOT need to be implemented | |
| * on the NFT contract. Instead, it is meant to be | |
| * implemented on a contract that wants to indicate | |
| * that it can safely receive NFTs. | |
| */ | |
| interface IERC721Receiver { | |
| function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) external returns (bytes4); | |
| } | |
| /** | |
| * This contract implements the IERC721Receiver interface. | |
| * So, it is here just for a SUCCESSFUL demo of | |
| * safeTransferFrom. | |
| * The 'to' argument in | |
| * safeTransferFrom(from, to, tokenId, data) | |
| * should be the address of this contract | |
| */ | |
| contract ERC721ReceiverMinimalExample is IERC721Receiver { | |
| function doSomethingWithReceivedNFT(address operator, address from, uint256 tokenId, bytes memory data) public { | |
| //Do something with received NFT | |
| } | |
| function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data) public returns (bytes4) { | |
| doSomethingWithReceivedNFT(operator, from, tokenId, data); | |
| return IERC721Receiver.onERC721Received.selector; // returns 0x150b7a02 | |
| } | |
| } | |
| /** | |
| * This contract DOES NOT implement the IERC721Receiver interface. | |
| * So, it is here just for a FAILING demo of | |
| * safeTransferFrom. | |
| * The 'to' argument in | |
| * safeTransferFrom(from, to, tokenId, data) | |
| * should be the address of this contract | |
| */ | |
| contract NonERC721ReceiverMinimalExample { | |
| function doSomethingWithReceivedNFT() public pure returns (string memory){ | |
| return "Hello"; | |
| } | |
| } | |
| /** | |
| * The actual contract implementing the NFT standard | |
| * i.e. ERC721. | |
| * Additionally, it also implements the optional | |
| * IERC721Metadata and IERC721Enumerable extensions | |
| * | |
| */ | |
| contract TigertNFT is IERC165, IERC721, IERC721Metadata, IERC721Enumerable { | |
| // ERC165 interface IDs | |
| bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; | |
| bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd; | |
| bytes4 private constant _INTERFACE_ID_ERC721Metadata = 0x5b5e139f; | |
| bytes4 private constant _INTERFACE_ID_ERC721Enumerable = 0x780e9d63; | |
| string public constant name = "TigerNFT"; | |
| string public constant symbol = "TNFT"; | |
| uint256 private _currentTokenId = 1; | |
| mapping(uint256 tokenId => address owner) private _owners; | |
| mapping(address owner => uint256 balance) private _balances; | |
| mapping(uint256 tokenId => string url) private _tokenURIs; | |
| mapping(uint256 tokenId => address spender) private _tokenApprovals; | |
| mapping(address owner => mapping(address operator => bool approved)) private _operatorApprovals; | |
| mapping(address owner => uint256[] tokens) private _ownedTokens; | |
| mapping(uint256 tokenId => uint256 index) private _ownedTokensIndex; | |
| uint256[] private _allTokens; | |
| mapping(uint256 tokenId => uint256 index) private _allTokensIndex; | |
| function getTokensOfOwner(address owner) public view returns(uint256[] memory) { | |
| return _ownedTokens[owner]; | |
| } | |
| function supportsInterface(bytes4 interfaceId) public pure returns (bool) { | |
| return interfaceId == _INTERFACE_ID_ERC165 || | |
| interfaceId == _INTERFACE_ID_ERC721 || | |
| interfaceId == _INTERFACE_ID_ERC721Metadata || | |
| interfaceId == _INTERFACE_ID_ERC721Enumerable; | |
| } | |
| function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { | |
| _ownedTokens[to].push(tokenId); | |
| _ownedTokensIndex[tokenId] = _ownedTokens[to].length - 1; | |
| } | |
| function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { | |
| uint256 tokenIndex = _ownedTokensIndex[tokenId]; | |
| uint256 lastIndex = _ownedTokens[from].length - 1; | |
| /** | |
| * Need to do a bit of moving around, if the | |
| * token being removed is not the last one | |
| */ | |
| if(tokenIndex < lastIndex) { | |
| uint256 lastToken = _ownedTokens[from][lastIndex]; | |
| _ownedTokens[from][tokenIndex] = lastToken; | |
| _ownedTokensIndex[lastToken] = tokenIndex; | |
| } | |
| _ownedTokens[from].pop(); | |
| delete _ownedTokensIndex[tokenId]; | |
| } | |
| function _updateTokenOwnerEnumeration(address from, address to, uint256 tokenId) private { | |
| if(from != address(0)) { | |
| _removeTokenFromOwnerEnumeration(from, tokenId); | |
| } | |
| if(to != address(0)) { | |
| _addTokenToOwnerEnumeration(to, tokenId); | |
| } | |
| } | |
| function transferFrom(address from, address to, uint256 tokenId) public payable { | |
| require(to != address(0), "Zero address"); | |
| address owner = ownerOf(tokenId); | |
| require(owner == from, "Token not owned by sender"); | |
| // Caller should either be owner, or approved or operator | |
| require(owner == msg.sender || | |
| getApproved(tokenId) == msg.sender || | |
| isApprovedForAll(from, msg.sender), | |
| "Not authorized" | |
| ); | |
| _updateTokenOwnerEnumeration(from, to, tokenId); | |
| _owners[tokenId] = to; | |
| _balances[from] -= 1; | |
| _balances[to] += 1; | |
| delete _tokenApprovals[tokenId]; | |
| emit Transfer(from, to, tokenId); | |
| } | |
| function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public payable { | |
| transferFrom(from, to, tokenId); | |
| require(_checkOnERC721Received(msg.sender, from, to, tokenId, data), "Not safe"); | |
| } | |
| function safeTransferFrom(address from, address to, uint256 tokenId) public payable { | |
| transferFrom(from, to, tokenId); | |
| require(_checkOnERC721Received(msg.sender, from, to, tokenId, ""), "Not safe"); | |
| } | |
| function _checkOnERC721Received(address operator, address from, address to, uint256 tokenId, bytes memory data) private returns (bool) { | |
| if(_isContract(to)) { | |
| try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) { | |
| return retval == IERC721Receiver.onERC721Received.selector; | |
| } | |
| catch { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function _isContract(address account) private view returns (bool) { | |
| return account.code.length > 0; | |
| } | |
| function isApprovedForAll(address owner, address operator) public view returns (bool) { | |
| return _operatorApprovals[owner][operator]; | |
| } | |
| function getApproved(uint256 tokenId) public view returns (address) { | |
| return _tokenApprovals[tokenId]; | |
| } | |
| function balanceOf(address owner) public view returns (uint256) { | |
| return _balances[owner]; | |
| } | |
| function ownerOf(uint256 tokenId) public view returns (address) { | |
| return _owners[tokenId]; | |
| } | |
| function approve(address spender, uint256 tokenId) public payable{ | |
| require(ownerOf(tokenId) == msg.sender || isApprovedForAll(msg.sender, spender), "Not authorized"); | |
| _tokenApprovals[tokenId] = spender; | |
| emit Approval(msg.sender, spender, tokenId); | |
| } | |
| function setApprovalForAll(address operator, bool approved) public { | |
| require(operator != address(0), "Zero address"); | |
| _operatorApprovals[msg.sender][operator] = approved; | |
| emit ApprovalForAll(msg.sender, operator, approved); | |
| } | |
| //Metadata functions | |
| function tokenURI(uint256 tokenId) public view returns (string memory) { | |
| return _tokenURIs[tokenId]; | |
| } | |
| /* Enumeration functions */ | |
| function totalSupply() public view returns (uint256) { | |
| return _allTokens.length; | |
| } | |
| function tokenOfOwnerByIndex(address owner, uint256 index) public view returns (uint256) { | |
| return _ownedTokens[owner][index]; | |
| } | |
| function tokenByIndex(uint256 index) public view returns (uint256) { | |
| return _allTokens[index]; | |
| } | |
| function mint(string memory ipfsUrl) public { | |
| uint256 tokenId = _currentTokenId++; | |
| _owners[tokenId] = msg.sender; | |
| _balances[msg.sender] += 1; | |
| _allTokens.push(tokenId); | |
| _allTokensIndex[tokenId] = _allTokens.length - 1; | |
| _tokenURIs[tokenId] = ipfsUrl; | |
| _addTokenToOwnerEnumeration(msg.sender, tokenId); | |
| emit Transfer(address(0), msg.sender, tokenId); | |
| } | |
| function burn(uint256 tokenId) public { | |
| address owner = ownerOf(tokenId); | |
| require(owner != address(0), "Not minted"); | |
| require(msg.sender == owner, "Not authorized"); | |
| _balances[owner] -= 1; | |
| delete _owners[tokenId]; | |
| delete _tokenURIs[tokenId]; | |
| delete _tokenApprovals[tokenId]; | |
| _removeTokenFromOwnerEnumeration(owner, tokenId); | |
| uint256 tokenIndex = _allTokensIndex[tokenId]; | |
| uint256 lastTokenIndex = totalSupply() - 1; | |
| if(tokenIndex < lastTokenIndex) { | |
| uint256 lastTokenId = _allTokens[lastTokenIndex]; | |
| _allTokens[tokenIndex] = _allTokens[lastTokenIndex]; | |
| _allTokensIndex[lastTokenId] = tokenIndex; | |
| } | |
| _allTokens.pop(); | |
| delete _allTokensIndex[tokenId]; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment