ERC721 Non-Fungible TokenΒΆ

Warning

This is example code for learning purposes. Do not use in production without thorough review and testing.

A standard ERC721 (NFT) implementation with minting and burning.

  1#pragma version >0.3.10
  2
  3###########################################################################
  4## THIS IS EXAMPLE CODE, NOT MEANT TO BE USED IN PRODUCTION! CAVEAT EMPTOR!
  5###########################################################################
  6
  7# @dev example implementation of ERC-721 non-fungible token standard.
  8# @author Ryuya Nakamura (@nrryuya)
  9# Modified from: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy
 10
 11from ethereum.ercs import IERC165
 12from ethereum.ercs import IERC721
 13
 14implements: IERC721
 15implements: IERC165
 16
 17# Interface for the contract called by safeTransferFrom()
 18interface ERC721Receiver:
 19    def onERC721Received(
 20            _operator: address,
 21            _from: address,
 22            _tokenId: uint256,
 23            _data: Bytes[1024]
 24        ) -> bytes4: nonpayable
 25
 26
 27# @dev Mapping from NFT ID to the address that owns it.
 28idToOwner: HashMap[uint256, address]
 29
 30# @dev Mapping from NFT ID to approved address.
 31idToApprovals: HashMap[uint256, address]
 32
 33# @dev Mapping from owner address to count of his tokens.
 34ownerToNFTokenCount: HashMap[address, uint256]
 35
 36# @dev Mapping from owner address to mapping of operator addresses.
 37ownerToOperators: HashMap[address, HashMap[address, bool]]
 38
 39# @dev Address of minter, who can mint a token
 40minter: address
 41
 42baseURL: String[53]
 43
 44# @dev Static list of supported ERC165 interface ids
 45SUPPORTED_INTERFACES: constant(bytes4[2]) = [
 46    # ERC165 interface ID of ERC165
 47    0x01ffc9a7,
 48    # ERC165 interface ID of ERC721
 49    0x80ac58cd,
 50]
 51
 52@deploy
 53def __init__():
 54    """
 55    @dev Contract constructor.
 56    """
 57    self.minter = msg.sender
 58    self.baseURL = "https://api.babby.xyz/metadata/"
 59
 60
 61@view
 62@external
 63def supportsInterface(interface_id: bytes4) -> bool:
 64    """
 65    @dev Interface identification is specified in ERC-165.
 66    @param interface_id Id of the interface
 67    """
 68    return interface_id in SUPPORTED_INTERFACES
 69
 70
 71### VIEW FUNCTIONS ###
 72
 73@view
 74@external
 75def balanceOf(_owner: address) -> uint256:
 76    """
 77    @dev Returns the number of NFTs owned by `_owner`.
 78         Throws if `_owner` is the zero address. NFTs assigned to the zero address are considered invalid.
 79    @param _owner Address for whom to query the balance.
 80    """
 81    assert _owner != empty(address)
 82    return self.ownerToNFTokenCount[_owner]
 83
 84
 85@view
 86@external
 87def ownerOf(_tokenId: uint256) -> address:
 88    """
 89    @dev Returns the address of the owner of the NFT.
 90         Throws if `_tokenId` is not a valid NFT.
 91    @param _tokenId The identifier for an NFT.
 92    """
 93    owner: address = self.idToOwner[_tokenId]
 94    # Throws if `_tokenId` is not a valid NFT
 95    assert owner != empty(address)
 96    return owner
 97
 98
 99@view
100@external
101def getApproved(_tokenId: uint256) -> address:
102    """
103    @dev Get the approved address for a single NFT.
104         Throws if `_tokenId` is not a valid NFT.
105    @param _tokenId ID of the NFT to query the approval of.
106    """
107    # Throws if `_tokenId` is not a valid NFT
108    assert self.idToOwner[_tokenId] != empty(address)
109    return self.idToApprovals[_tokenId]
110
111
112@view
113@external
114def isApprovedForAll(_owner: address, _operator: address) -> bool:
115    """
116    @dev Checks if `_operator` is an approved operator for `_owner`.
117    @param _owner The address that owns the NFTs.
118    @param _operator The address that acts on behalf of the owner.
119    """
120    return (self.ownerToOperators[_owner])[_operator]
121
122
123### TRANSFER FUNCTION HELPERS ###
124
125@view
126@internal
127def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
128    """
129    @dev Returns whether the given spender can transfer a given token ID
130    @param spender address of the spender to query
131    @param tokenId uint256 ID of the token to be transferred
132    @return bool whether the msg.sender is approved for the given token ID,
133        is an operator of the owner, or is the owner of the token
134    """
135    owner: address = self.idToOwner[_tokenId]
136    spenderIsOwner: bool = owner == _spender
137    spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
138    spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
139    return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll
140
141
142@internal
143def _addTokenTo(_to: address, _tokenId: uint256):
144    """
145    @dev Add a NFT to a given address
146         Throws if `_tokenId` is owned by someone.
147    """
148    # Throws if `_tokenId` is owned by someone
149    assert self.idToOwner[_tokenId] == empty(address)
150    # Change the owner
151    self.idToOwner[_tokenId] = _to
152    # Change count tracking
153    self.ownerToNFTokenCount[_to] += 1
154
155
156@internal
157def _removeTokenFrom(_from: address, _tokenId: uint256):
158    """
159    @dev Remove a NFT from a given address
160         Throws if `_from` is not the current owner.
161    """
162    # Throws if `_from` is not the current owner
163    assert self.idToOwner[_tokenId] == _from
164    # Change the owner
165    self.idToOwner[_tokenId] = empty(address)
166    # Change count tracking
167    self.ownerToNFTokenCount[_from] -= 1
168
169
170@internal
171def _clearApproval(_owner: address, _tokenId: uint256):
172    """
173    @dev Clear an approval of a given address
174         Throws if `_owner` is not the current owner.
175    """
176    # Throws if `_owner` is not the current owner
177    assert self.idToOwner[_tokenId] == _owner
178    if self.idToApprovals[_tokenId] != empty(address):
179        # Reset approvals
180        self.idToApprovals[_tokenId] = empty(address)
181
182
183@internal
184def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
185    """
186    @dev Exeute transfer of a NFT.
187         Throws unless `msg.sender` is the current owner, an authorized operator, or the approved
188         address for this NFT. (NOTE: `msg.sender` not allowed in private function so pass `_sender`.)
189         Throws if `_to` is the zero address.
190         Throws if `_from` is not the current owner.
191         Throws if `_tokenId` is not a valid NFT.
192    """
193    # Check requirements
194    assert self._isApprovedOrOwner(_sender, _tokenId)
195    # Throws if `_to` is the zero address
196    assert _to != empty(address)
197    # Clear approval. Throws if `_from` is not the current owner
198    self._clearApproval(_from, _tokenId)
199    # Remove NFT. Throws if `_tokenId` is not a valid NFT
200    self._removeTokenFrom(_from, _tokenId)
201    # Add NFT
202    self._addTokenTo(_to, _tokenId)
203    # Log the transfer
204    log IERC721.Transfer(sender=_from, receiver=_to, token_id=_tokenId)
205
206
207### TRANSFER FUNCTIONS ###
208
209@external
210@payable
211def transferFrom(_from: address, _to: address, _tokenId: uint256):
212    """
213    @dev Throws unless `msg.sender` is the current owner, an authorized operator, or the approved
214         address for this NFT.
215         Throws if `_from` is not the current owner.
216         Throws if `_to` is the zero address.
217         Throws if `_tokenId` is not a valid NFT.
218    @notice The caller is responsible to confirm that `_to` is capable of receiving NFTs or else
219            they maybe be permanently lost.
220    @param _from The current owner of the NFT.
221    @param _to The new owner.
222    @param _tokenId The NFT to transfer.
223    """
224    self._transferFrom(_from, _to, _tokenId, msg.sender)
225
226
227@external
228@payable
229def safeTransferFrom(
230        _from: address,
231        _to: address,
232        _tokenId: uint256,
233        _data: Bytes[1024]=b""
234    ):
235    """
236    @dev Transfers the ownership of an NFT from one address to another address.
237         Throws unless `msg.sender` is the current owner, an authorized operator, or the
238         approved address for this NFT.
239         Throws if `_from` is not the current owner.
240         Throws if `_to` is the zero address.
241         Throws if `_tokenId` is not a valid NFT.
242         If `_to` is a smart contract, it calls `onERC721Received` on `_to` and throws if
243         the return value is not `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
244    @param _from The current owner of the NFT.
245    @param _to The new owner.
246    @param _tokenId The NFT to transfer.
247    @param _data Additional data with no specified format, sent in call to `_to`.
248    """
249    self._transferFrom(_from, _to, _tokenId, msg.sender)
250    if _to.is_contract: # check if `_to` is a contract address
251        returnValue: bytes4 = extcall ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)
252        # Throws if transfer destination is a contract which does not implement 'onERC721Received'
253        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4)
254
255
256@external
257@payable
258def approve(_approved: address, _tokenId: uint256):
259    """
260    @dev Set or reaffirm the approved address for an NFT. The zero address indicates there is no approved address.
261         Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner.
262         Throws if `_tokenId` is not a valid NFT. (NOTE: This is not written the EIP)
263         Throws if `_approved` is the current owner. (NOTE: This is not written the EIP)
264    @param _approved Address to be approved for the given NFT ID.
265    @param _tokenId ID of the token to be approved.
266    """
267    owner: address = self.idToOwner[_tokenId]
268    # Throws if `_tokenId` is not a valid NFT
269    assert owner != empty(address)
270    # Throws if `_approved` is the current owner
271    assert _approved != owner
272    # Check requirements
273    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
274    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
275    assert (senderIsOwner or senderIsApprovedForAll)
276    # Set the approval
277    self.idToApprovals[_tokenId] = _approved
278    log IERC721.Approval(owner=owner, approved=_approved, token_id=_tokenId)
279
280
281@external
282def setApprovalForAll(_operator: address, _approved: bool):
283    """
284    @dev Enables or disables approval for a third party ("operator") to manage all of
285         `msg.sender`'s assets. It also emits the ApprovalForAll event.
286         Throws if `_operator` is the `msg.sender`. (NOTE: This is not written the EIP)
287    @notice This works even if sender doesn't own any tokens at the time.
288    @param _operator Address to add to the set of authorized operators.
289    @param _approved True if the operators is approved, false to revoke approval.
290    """
291    # Throws if `_operator` is the `msg.sender`
292    assert _operator != msg.sender
293    self.ownerToOperators[msg.sender][_operator] = _approved
294    log IERC721.ApprovalForAll(owner=msg.sender, operator=_operator, approved=_approved)
295
296
297### MINT & BURN FUNCTIONS ###
298
299@external
300def mint(_to: address, _tokenId: uint256) -> bool:
301    """
302    @dev Function to mint tokens
303         Throws if `msg.sender` is not the minter.
304         Throws if `_to` is zero address.
305         Throws if `_tokenId` is owned by someone.
306    @param _to The address that will receive the minted tokens.
307    @param _tokenId The token id to mint.
308    @return A boolean that indicates if the operation was successful.
309    """
310    # Throws if `msg.sender` is not the minter
311    assert msg.sender == self.minter
312    # Throws if `_to` is zero address
313    assert _to != empty(address)
314    # Add NFT. Throws if `_tokenId` is owned by someone
315    self._addTokenTo(_to, _tokenId)
316    log IERC721.Transfer(sender=empty(address), receiver=_to, token_id=_tokenId)
317    return True
318
319
320@external
321def burn(_tokenId: uint256):
322    """
323    @dev Burns a specific ERC721 token.
324         Throws unless `msg.sender` is the current owner, an authorized operator, or the approved
325         address for this NFT.
326         Throws if `_tokenId` is not a valid NFT.
327    @param _tokenId uint256 id of the ERC721 token to be burned.
328    """
329    # Check requirements
330    assert self._isApprovedOrOwner(msg.sender, _tokenId)
331    owner: address = self.idToOwner[_tokenId]
332    # Throws if `_tokenId` is not a valid NFT
333    assert owner != empty(address)
334    self._clearApproval(owner, _tokenId)
335    self._removeTokenFrom(owner, _tokenId)
336    log IERC721.Transfer(sender=owner, receiver=empty(address), token_id=_tokenId)
337
338
339@view
340@external
341def tokenURI(tokenId: uint256) -> String[132]:
342    return concat(self.baseURL, uint2str(tokenId))

This implementation includes:

  • mint and burn functions controlled by a minter address

  • safeTransferFrom with receiver callback verification

  • Operator approval via setApprovalForAll

  • ERC165 interface detection

The safeTransferFrom function checks if the recipient is a contract and, if so, calls onERC721Received to ensure the recipient can handle NFTs.