Created
March 5, 2023 01:12
-
-
Save cygaar/019cbcfff2d56129ad9aa0b85955eb5b to your computer and use it in GitHub Desktop.
Sample NFT contract written in Vyper
This file contains 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
# @version 0.3.7 | |
# @dev Implementation of ERC-721 non-fungible token standard. | |
# Modified from: https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy | |
from vyper.interfaces import ERC165 | |
from vyper.interfaces import ERC721 | |
implements: ERC721 | |
implements: ERC165 | |
# Interface for the contract called by safeTransferFrom() | |
interface ERC721Receiver: | |
def onERC721Received( | |
_operator: address, | |
_from: address, | |
_tokenId: uint256, | |
_data: Bytes[1024] | |
) -> bytes4: nonpayable | |
# @dev Emits when ownership of any NFT changes by any mechanism. This event emits when NFTs are | |
# created (`from` == 0) and destroyed (`to` == 0). Exception: during contract creation, any | |
# number of NFTs may be created and assigned without emitting Transfer. At the time of any | |
# transfer, the approved address for that NFT (if any) is reset to none. | |
# @param _from Sender of NFT (if address is zero address it indicates token creation). | |
# @param _to Receiver of NFT (if address is zero address it indicates token destruction). | |
# @param _tokenId The NFT that got transfered. | |
event Transfer: | |
sender: indexed(address) | |
receiver: indexed(address) | |
tokenId: indexed(uint256) | |
# @dev This emits when the approved address for an NFT is changed or reaffirmed. The zero | |
# address indicates there is no approved address. When a Transfer event emits, this also | |
# indicates that the approved address for that NFT (if any) is reset to none. | |
# @param _owner Owner of NFT. | |
# @param _approved Address that we are approving. | |
# @param _tokenId NFT which we are approving. | |
event Approval: | |
owner: indexed(address) | |
approved: indexed(address) | |
tokenId: indexed(uint256) | |
# @dev This emits when an operator is enabled or disabled for an owner. The operator can manage | |
# all NFTs of the owner. | |
# @param _owner Owner of NFT. | |
# @param _operator Address to which we are setting operator rights. | |
# @param _approved Status of operator rights(true if operator rights are given and false if | |
# revoked). | |
event ApprovalForAll: | |
owner: indexed(address) | |
operator: indexed(address) | |
approved: bool | |
# @dev Returns the token collection name. | |
# @notice If you declare a variable as `public`, | |
# Vyper automatically generates an `external` | |
# getter function for the variable. Furthermore, | |
# to preserve consistency with the interface for | |
# the optional metadata functions of the ERC-721 | |
# standard, we use lower case letters for the | |
# `immutable` variables `name` and `symbol`. | |
name: public(immutable(String[25])) | |
# @dev Returns the token collection symbol. | |
# @notice See comment on lower case letters | |
# above at `name`. | |
symbol: public(immutable(String[10])) | |
# @dev Mapping from NFT ID to the address that owns it. | |
idToOwner: HashMap[uint256, address] | |
# @dev Mapping from NFT ID to approved address. | |
idToApprovals: HashMap[uint256, address] | |
# @dev Mapping from owner address to count of his tokens. | |
ownerToNFTokenCount: HashMap[address, uint256] | |
# @dev Mapping from owner address to mapping of operator addresses. | |
ownerToOperators: HashMap[address, HashMap[address, bool]] | |
# @dev Address of minter, who can mint a token | |
minter: address | |
baseURL: String[53] | |
# @dev Static list of supported ERC165 interface ids | |
SUPPORTED_INTERFACES: constant(bytes4[2]) = [ | |
# ERC165 interface ID of ERC165 | |
0x01ffc9a7, | |
# ERC165 interface ID of ERC721 | |
0x80ac58cd, | |
] | |
@external | |
def __init__(): | |
""" | |
@dev Contract constructor. | |
""" | |
self.minter = msg.sender | |
name = "Sample Vyper NFT" | |
symbol = "SAMPLE" | |
@pure | |
@external | |
def supportsInterface(interface_id: bytes4) -> bool: | |
""" | |
@dev Interface identification is specified in ERC-165. | |
@param interface_id Id of the interface | |
""" | |
return interface_id in SUPPORTED_INTERFACES | |
### VIEW FUNCTIONS ### | |
@view | |
@external | |
def balanceOf(_owner: address) -> uint256: | |
""" | |
@dev Returns the number of NFTs owned by `_owner`. | |
Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid. | |
@param _owner Address for whom to query the balance. | |
""" | |
assert _owner != empty(address) | |
return self.ownerToNFTokenCount[_owner] | |
@view | |
@external | |
def ownerOf(_tokenId: uint256) -> address: | |
""" | |
@dev Returns the address of the owner of the NFT. | |
Throws if `_tokenId` is not a valid NFT. | |
@param _tokenId The identifier for an NFT. | |
""" | |
owner: address = self.idToOwner[_tokenId] | |
# Throws if `_tokenId` is not a valid NFT | |
assert owner != empty(address) | |
return owner | |
@view | |
@external | |
def getApproved(_tokenId: uint256) -> address: | |
""" | |
@dev Get the approved address for a single NFT. | |
Throws if `_tokenId` is not a valid NFT. | |
@param _tokenId ID of the NFT to query the approval of. | |
""" | |
# Throws if `_tokenId` is not a valid NFT | |
assert self.idToOwner[_tokenId] != empty(address) | |
return self.idToApprovals[_tokenId] | |
@view | |
@external | |
def isApprovedForAll(_owner: address, _operator: address) -> bool: | |
""" | |
@dev Checks if `_operator` is an approved operator for `_owner`. | |
@param _owner The address that owns the NFTs. | |
@param _operator The address that acts on behalf of the owner. | |
""" | |
return (self.ownerToOperators[_owner])[_operator] | |
### TRANSFER FUNCTION HELPERS ### | |
@view | |
@internal | |
def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool: | |
""" | |
@dev Returns whether the given spender can transfer a given token ID | |
@param spender address of the spender to query | |
@param tokenId uint256 ID of the token to be transferred | |
@return bool whether the msg.sender is approved for the given token ID, | |
is an operator of the owner, or is the owner of the token | |
""" | |
owner: address = self.idToOwner[_tokenId] | |
spenderIsOwner: bool = owner == _spender | |
spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId] | |
spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender] | |
return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll | |
@internal | |
def _addTokenTo(_to: address, _tokenId: uint256): | |
""" | |
@dev Add a NFT to a given address | |
Throws if `_tokenId` is owned by someone. | |
""" | |
# Throws if `_tokenId` is owned by someone | |
assert self.idToOwner[_tokenId] == empty(address) | |
# Change the owner | |
self.idToOwner[_tokenId] = _to | |
# Change count tracking | |
self.ownerToNFTokenCount[_to] += 1 | |
@internal | |
def _removeTokenFrom(_from: address, _tokenId: uint256): | |
""" | |
@dev Remove a NFT from a given address | |
Throws if `_from` is not the current owner. | |
""" | |
# Throws if `_from` is not the current owner | |
assert self.idToOwner[_tokenId] == _from | |
# Change the owner | |
self.idToOwner[_tokenId] = empty(address) | |
# Change count tracking | |
self.ownerToNFTokenCount[_from] -= 1 | |
@internal | |
def _clearApproval(_owner: address, _tokenId: uint256): | |
""" | |
@dev Clear an approval of a given address | |
Throws if `_owner` is not the current owner. | |
""" | |
# Throws if `_owner` is not the current owner | |
assert self.idToOwner[_tokenId] == _owner | |
if self.idToApprovals[_tokenId] != empty(address): | |
# Reset approvals | |
self.idToApprovals[_tokenId] = empty(address) | |
@internal | |
def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address): | |
""" | |
@dev Exeute transfer of a NFT. | |
Throws unless `msg.sender` is the current owner, an authorized operator, or the approved | |
address for this NFT. (NOTE: `msg.sender` not allowed in private function so pass `_sender`.) | |
Throws if `_to` is the zero address. | |
Throws if `_from` is not the current owner. | |
Throws if `_tokenId` is not a valid NFT. | |
""" | |
# Check requirements | |
assert self._isApprovedOrOwner(_sender, _tokenId) | |
# Throws if `_to` is the zero address | |
assert _to != empty(address) | |
# Clear approval. Throws if `_from` is not the current owner | |
self._clearApproval(_from, _tokenId) | |
# Remove NFT. Throws if `_tokenId` is not a valid NFT | |
self._removeTokenFrom(_from, _tokenId) | |
# Add NFT | |
self._addTokenTo(_to, _tokenId) | |
# Log the transfer | |
log Transfer(_from, _to, _tokenId) | |
### TRANSFER FUNCTIONS ### | |
@external | |
@payable | |
def transferFrom(_from: address, _to: address, _tokenId: uint256): | |
""" | |
@dev Throws unless `msg.sender` is the current owner, an authorized operator, or the approved | |
address for this NFT. | |
Throws if `_from` is not the current owner. | |
Throws if `_to` is the zero address. | |
Throws if `_tokenId` is not a valid NFT. | |
@notice The caller is responsible to confirm that `_to` is capable of receiving NFTs or else | |
they maybe be permanently lost. | |
@param _from The current owner of the NFT. | |
@param _to The new owner. | |
@param _tokenId The NFT to transfer. | |
""" | |
self._transferFrom(_from, _to, _tokenId, msg.sender) | |
@external | |
@payable | |
def safeTransferFrom( | |
_from: address, | |
_to: address, | |
_tokenId: uint256, | |
_data: Bytes[1024]=b"" | |
): | |
""" | |
@dev Transfers the ownership of an NFT from one address to another address. | |
Throws unless `msg.sender` is the current owner, an authorized operator, or the | |
approved address for this NFT. | |
Throws if `_from` is not the current owner. | |
Throws if `_to` is the zero address. | |
Throws if `_tokenId` is not a valid NFT. | |
If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if | |
the return value is not `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. | |
@param _from The current owner of the NFT. | |
@param _to The new owner. | |
@param _tokenId The NFT to transfer. | |
@param _data Additional data with no specified format, sent in call to `_to`. | |
""" | |
self._transferFrom(_from, _to, _tokenId, msg.sender) | |
if _to.is_contract: # check if `_to` is a contract address | |
returnValue: bytes4 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) | |
# Throws if transfer destination is a contract which does not implement 'onERC721Received' | |
assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4) | |
@external | |
@payable | |
def approve(_approved: address, _tokenId: uint256): | |
""" | |
@dev Set or reaffirm the approved address for an NFT. The zero address indicates there is no approved address. | |
Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner. | |
Throws if `_tokenId` is not a valid NFT. (NOTE: This is not written the EIP) | |
Throws if `_approved` is the current owner. (NOTE: This is not written the EIP) | |
@param _approved Address to be approved for the given NFT ID. | |
@param _tokenId ID of the token to be approved. | |
""" | |
owner: address = self.idToOwner[_tokenId] | |
# Throws if `_tokenId` is not a valid NFT | |
assert owner != empty(address) | |
# Throws if `_approved` is the current owner | |
assert _approved != owner | |
# Check requirements | |
senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender | |
senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender] | |
assert (senderIsOwner or senderIsApprovedForAll) | |
# Set the approval | |
self.idToApprovals[_tokenId] = _approved | |
log Approval(owner, _approved, _tokenId) | |
@external | |
def setApprovalForAll(_operator: address, _approved: bool): | |
""" | |
@dev Enables or disables approval for a third party ("operator") to manage all of | |
`msg.sender`'s assets. It also emits the ApprovalForAll event. | |
Throws if `_operator` is the `msg.sender`. (NOTE: This is not written the EIP) | |
@notice This works even if sender doesn't own any tokens at the time. | |
@param _operator Address to add to the set of authorized operators. | |
@param _approved True if the operators is approved, false to revoke approval. | |
""" | |
# Throws if `_operator` is the `msg.sender` | |
assert _operator != msg.sender | |
self.ownerToOperators[msg.sender][_operator] = _approved | |
log ApprovalForAll(msg.sender, _operator, _approved) | |
### MINT & BURN FUNCTIONS ### | |
@external | |
def mint(_to: address, _tokenId: uint256) -> bool: | |
""" | |
@dev Function to mint tokens | |
Throws if `msg.sender` is not the minter. | |
Throws if `_to` is zero address. | |
Throws if `_tokenId` is owned by someone. | |
@param _to The address that will receive the minted tokens. | |
@param _tokenId The token id to mint. | |
@return A boolean that indicates if the operation was successful. | |
""" | |
# Throws if `msg.sender` is not the minter | |
assert msg.sender == self.minter | |
# Throws if `_to` is zero address | |
assert _to != empty(address) | |
# Add NFT. Throws if `_tokenId` is owned by someone | |
self._addTokenTo(_to, _tokenId) | |
log Transfer(empty(address), _to, _tokenId) | |
return True | |
@external | |
def burn(_tokenId: uint256): | |
""" | |
@dev Burns a specific ERC721 token. | |
Throws unless `msg.sender` is the current owner, an authorized operator, or the approved | |
address for this NFT. | |
Throws if `_tokenId` is not a valid NFT. | |
@param _tokenId uint256 id of the ERC721 token to be burned. | |
""" | |
# Check requirements | |
assert self._isApprovedOrOwner(msg.sender, _tokenId) | |
owner: address = self.idToOwner[_tokenId] | |
# Throws if `_tokenId` is not a valid NFT | |
assert owner != empty(address) | |
self._clearApproval(owner, _tokenId) | |
self._removeTokenFrom(owner, _tokenId) | |
log Transfer(owner, empty(address), _tokenId) | |
@view | |
@external | |
def tokenURI(tokenId: uint256) -> String[132]: | |
return concat(self.baseURL, uint2str(tokenId)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment