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:
mintandburnfunctions controlled by a minter addresssafeTransferFromwith receiver callback verificationOperator approval via
setApprovalForAllERC165 interface detection
The safeTransferFrom function checks if the recipient is a contract and, if so,
calls onERC721Received to ensure the recipient can handle NFTs.